From 2e1570d67aaadbc4fcce798d946d921a8b9a9401 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 15 Nov 2024 13:24:13 +0100 Subject: [PATCH 01/37] Streamline tss config --- cmd/zetaclientd/start.go | 60 ++++++++-------------------------------- zetaclient/tss/config.go | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 49 deletions(-) create mode 100644 zetaclient/tss/config.go diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index 46d204fa34..aefadf34aa 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -2,9 +2,7 @@ package main import ( "context" - "encoding/json" "fmt" - "io" "os" "os/signal" "path/filepath" @@ -13,11 +11,9 @@ import ( "syscall" "time" - ecdsakeygen "github.com/bnb-chain/tss-lib/ecdsa/keygen" "github.com/cometbft/cometbft/crypto/secp256k1" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/p2p/protocol/ping" - maddr "github.com/multiformats/go-multiaddr" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -39,11 +35,9 @@ import ( "github.com/zeta-chain/node/zetaclient/zetacore" ) -// todo revamp +// Start starts zetaclientd process todo revamp // https://github.com/zeta-chain/node/issues/3119 // https://github.com/zeta-chain/node/issues/3112 -var preParams *ecdsakeygen.LocalPreParams - func Start(_ *cobra.Command, _ []string) error { // Prompt for Hotkey, TSS key-share and relayer key passwords titles := []string{"HotKey", "TSS", "Solana Relayer Key"} @@ -164,12 +158,16 @@ func Start(_ *cobra.Command, _ []string) error { } priKey := secp256k1.PrivKey(hotkeyPk.Bytes()[:32]) - // Generate pre Params if not present already - peers, err := initPeers(cfg.Peer) + tssBootstrapPeers, err := mc.MultiAddressFromString(cfg.Peer) + if err != nil { + // this is okay, we still have whitelisted peers to connect to + startLogger.Warn().Err(err).Msg("TSS bootstrap peers error") + } + + tssPreParams, err := mc.ResolvePreParamsFromPath(cfg.PreParamsPath) if err != nil { - log.Error().Err(err).Msg("peer address error") + return errors.Wrap(err, "unable to resolve TSS pre params. Use `zetaclient tss gen-pre-params`") } - initPreParams(cfg.PreParamsPath) m, err := metrics.NewMetrics() if err != nil { @@ -201,9 +199,9 @@ func Start(_ *cobra.Command, _ []string) error { // Create TSS server tssServer, err := mc.SetupTSSServer( - peers, + tssBootstrapPeers, priKey, - preParams, + tssPreParams, appContext.Config(), tssKeyPass, true, @@ -409,42 +407,6 @@ func Start(_ *cobra.Command, _ []string) error { return nil } -func initPeers(peer string) ([]maddr.Multiaddr, error) { - var peers []maddr.Multiaddr - - if peer != "" { - address, err := maddr.NewMultiaddr(peer) - if err != nil { - log.Error().Err(err).Msg("NewMultiaddr error") - return []maddr.Multiaddr{}, err - } - peers = append(peers, address) - } - return peers, nil -} - -func initPreParams(path string) { - if path != "" { - path = filepath.Clean(path) - log.Info().Msgf("pre-params file path %s", path) - preParamsFile, err := os.Open(path) - if err != nil { - log.Error().Err(err).Msg("open pre-params file failed; skip") - } else { - bz, err := io.ReadAll(preParamsFile) - if err != nil { - log.Error().Err(err).Msg("read pre-params file failed; skip") - } else { - err = json.Unmarshal(bz, &preParams) - if err != nil { - log.Error().Err(err).Msg("unmarshal pre-params file failed; skip and generate new one") - preParams = nil // skip reading pre-params; generate new one instead - } - } - } - } -} - // isObserverNode checks whether THIS node is an observer node. func isObserverNode(ctx context.Context, client *zetacore.Client) (bool, error) { observers, err := client.GetObserverList(ctx) diff --git a/zetaclient/tss/config.go b/zetaclient/tss/config.go new file mode 100644 index 0000000000..9d7601d49b --- /dev/null +++ b/zetaclient/tss/config.go @@ -0,0 +1,48 @@ +package tss + +import ( + "encoding/json" + "os" + "path/filepath" + + "github.com/bnb-chain/tss-lib/ecdsa/keygen" + "github.com/multiformats/go-multiaddr" + "github.com/pkg/errors" +) + +// MultiAddressFromString parses a string into a slice of addresses (for convenience). +func MultiAddressFromString(peer string) ([]multiaddr.Multiaddr, error) { + if peer == "" { + return nil, errors.New("peer is empty") + } + + ma, err := multiaddr.NewMultiaddr(peer) + if err != nil { + return nil, err + } + + return []multiaddr.Multiaddr{ma}, nil +} + +// ResolvePreParamsFromPath resolves TSS pre-params from json config by path. +// Error indicates that the pre-params file is not found or invalid. +// FYI: pre-params are generated by keygen.GeneratePreParams. +func ResolvePreParamsFromPath(path string) (*keygen.LocalPreParams, error) { + if path == "" { + return nil, errors.New("pre-params path is empty") + } + + path = filepath.Clean(path) + + raw, err := os.ReadFile(path) + if err != nil { + return nil, errors.Wrapf(err, "unable to read pre-params at %q", path) + } + + var pp keygen.LocalPreParams + if err = json.Unmarshal(raw, &pp); err != nil { + return nil, errors.Wrapf(err, "unable to decode pre-params at %q", path) + } + + return &pp, nil +} From 969f17d6750d859537662caf7780d7dec9ffda2f Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:39:05 +0100 Subject: [PATCH 02/37] Revamp TSS keygen ceremony code --- cmd/zetaclientd/start.go | 13 +- zetaclient/tss/config.go | 6 + zetaclient/tss/generate.go | 198 ------------------------------ zetaclient/tss/keygen.go | 231 +++++++++++++++++++++++++++++++++++ zetaclient/tss/tss_signer.go | 2 +- 5 files changed, 246 insertions(+), 204 deletions(-) delete mode 100644 zetaclient/tss/generate.go create mode 100644 zetaclient/tss/keygen.go diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index aefadf34aa..b8cc72d23f 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -254,9 +254,9 @@ func Start(_ *cobra.Command, _ []string) error { // Generate a new TSS if keygen is set and add it into the tss server // If TSS has already been generated, and keygen was successful ; we use the existing TSS - err = mc.Generate(ctx, zetacoreClient, tssServer, masterLogger) + err = mc.KeygenCeremony(ctx, tssServer, zetacoreClient, masterLogger) if err != nil { - return err + return errors.Wrap(err, "unable to run tss keygen ceremony") } tss, err := mc.New( @@ -271,9 +271,12 @@ func Start(_ *cobra.Command, _ []string) error { return err } if cfg.TestTssKeysign { - err = mc.TestTSS(tss.CurrentPubkey, *tss.Server, masterLogger) - if err != nil { - startLogger.Error().Err(err).Msgf("TestTSS error : %s", tss.CurrentPubkey) + startLogger.Info().Msg("Performing TSS key-sign test") + + if err = mc.TestKeysign(tss.CurrentPubkey, *tss.Server); err != nil { + startLogger.Error().Err(err). + Str("tss.public_key", tss.CurrentPubkey). + Msg("TSS key-sign failed") } } diff --git a/zetaclient/tss/config.go b/zetaclient/tss/config.go index 9d7601d49b..dffcb17b51 100644 --- a/zetaclient/tss/config.go +++ b/zetaclient/tss/config.go @@ -8,6 +8,12 @@ import ( "github.com/bnb-chain/tss-lib/ecdsa/keygen" "github.com/multiformats/go-multiaddr" "github.com/pkg/errors" + tsscommon "gitlab.com/thorchain/tss/go-tss/common" +) + +const ( + Version = "0.14.0" + Algo = tsscommon.ECDSA ) // MultiAddressFromString parses a string into a slice of addresses (for convenience). diff --git a/zetaclient/tss/generate.go b/zetaclient/tss/generate.go deleted file mode 100644 index 1adaad8c5c..0000000000 --- a/zetaclient/tss/generate.go +++ /dev/null @@ -1,198 +0,0 @@ -package tss - -import ( - "context" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "time" - - "github.com/rs/zerolog" - tsscommon "gitlab.com/thorchain/tss/go-tss/common" - "gitlab.com/thorchain/tss/go-tss/keygen" - "gitlab.com/thorchain/tss/go-tss/tss" - "golang.org/x/crypto/sha3" - - "github.com/zeta-chain/node/pkg/chains" - observertypes "github.com/zeta-chain/node/x/observer/types" - "github.com/zeta-chain/node/zetaclient/chains/interfaces" - "github.com/zeta-chain/node/zetaclient/logs" - "github.com/zeta-chain/node/zetaclient/metrics" - "github.com/zeta-chain/node/zetaclient/zetacore" -) - -// Generate generates a new TSS if keygen is set. -// If a TSS was generated successfully in the past,and the keygen was successful, the function will return without doing anything. -// If a keygen has been set the functions will wait for the correct block to arrive and generate a new TSS. -// In case of a successful keygen a TSS success vote is broadcasted to zetacore and the newly generate TSS is tested. The generated keyshares are stored in the correct directory -// In case of a failed keygen a TSS failed vote is broadcasted to zetacore. -func Generate( - ctx context.Context, - zc *zetacore.Client, - keygenTssServer *tss.TssServer, - logger zerolog.Logger, -) error { - keygenLogger := logger.With().Str(logs.FieldModule, "tss_keygen").Logger() - // If Keygen block is set it will try to generate new TSS at the block - // This is a blocking thread and will wait until the ceremony is complete successfully - // If the TSS generation is unsuccessful , it will loop indefinitely until a new TSS is generated - // Set TSS block to 0 using genesis file to disable this feature - // Note : The TSS generation is done through the "hotkey" or "Zeta-clientGrantee" This key needs to be present on the machine for the TSS signing to happen . - // "ZetaClientGrantee" key is different from the "operator" key .The "Operator" key gives all zetaclient related permissions such as TSS generation ,reporting and signing, INBOUND and OUTBOUND vote signing, to the "ZetaClientGrantee" key. - // The votes to signify a successful TSS generation (Or unsuccessful) is signed by the operator key and broadcast to zetacore by the zetcalientGrantee key on behalf of the operator . - ticker := time.NewTicker(time.Second * 1) - triedKeygenAtBlock := false - lastBlock := int64(0) - for range ticker.C { - // Break out of loop only when TSS is generated successfully, either at the keygenBlock or if it has been generated already , Block set as zero in genesis file - // This loop will try keygen at the keygen block and then wait for keygen to be successfully reported by all nodes before breaking out of the loop. - // If keygen is unsuccessful, it will reset the triedKeygenAtBlock flag and try again at a new keygen block. - keyGen, err := zc.GetKeyGen(ctx) - switch { - case err != nil: - keygenLogger.Error().Err(err).Msg("GetKeyGen RPC error") - continue - case keyGen.Status == observertypes.KeygenStatus_KeyGenSuccess: - return nil - case keyGen.Status == observertypes.KeygenStatus_KeyGenFailed: - // Arrive at this stage only if keygen is unsuccessfully reported by every node. - // This will reset the flag and to try again at a new keygen block - triedKeygenAtBlock = false - continue - } - - // Try generating TSS at keygen block , only when status is pending keygen and generation has not been tried at the block - if keyGen.Status == observertypes.KeygenStatus_PendingKeygen { - // Return error if RPC is not working - currentBlock, err := zc.GetBlockHeight(ctx) - if err != nil { - keygenLogger.Error().Err(err).Msg("GetBlockHeight RPC error") - continue - } - // Reset the flag if the keygen block has passed and a new keygen block has been set . This condition is only reached if the older keygen is stuck at PendingKeygen for some reason - if keyGen.BlockNumber > currentBlock { - triedKeygenAtBlock = false - } - if !triedKeygenAtBlock { - // If not at keygen block do not try to generate TSS - if currentBlock != keyGen.BlockNumber { - if currentBlock > lastBlock { - lastBlock = currentBlock - keygenLogger.Info(). - Msgf("Waiting For Keygen Block to arrive or new keygen block to be set. Keygen Block: %d; Current Block: %d", keyGen.BlockNumber, currentBlock) - } - continue - } - // Try keygen only once at a particular block, irrespective of whether it is successful or failure - triedKeygenAtBlock = true - newPubkey, err := keygenTSS(ctx, keyGen, *keygenTssServer, zc, keygenLogger) - if err != nil { - keygenLogger.Error().Err(err).Msg("keygenTSS error") - tssFailedVoteHash, err := zc.PostVoteTSS(ctx, - "", keyGen.BlockNumber, chains.ReceiveStatus_failed) - if err != nil { - keygenLogger.Error().Err(err).Msg("Failed to broadcast Failed TSS Vote to zetacore") - return err - } - keygenLogger.Info().Msgf("TSS Failed Vote: %s", tssFailedVoteHash) - continue - } - // If TSS is successful , broadcast the vote to zetacore and also set the Pubkey - tssSuccessVoteHash, err := zc.PostVoteTSS(ctx, - newPubkey, - keyGen.BlockNumber, - chains.ReceiveStatus_success, - ) - if err != nil { - keygenLogger.Error().Err(err).Msg("TSS successful but unable to broadcast vote to zeta-core") - return err - } - keygenLogger.Info().Msgf("TSS successful Vote: %s", tssSuccessVoteHash) - - err = TestTSS(newPubkey, *keygenTssServer, keygenLogger) - if err != nil { - keygenLogger.Error().Err(err).Msgf("TestTSS error: %s", newPubkey) - } - continue - } - } - keygenLogger.Debug(). - Msgf("Waiting for TSS to be generated or Current Keygen to be be finalized. Keygen Block: %d", keyGen.BlockNumber) - } - return errors.New("unexpected state for TSS generation") -} - -// keygenTSS generates a new TSS using the keygen request and the TSS server. -// If the keygen is successful, the function returns the new TSS pubkey. -// If the keygen is unsuccessful, the function posts blame and returns an error. -func keygenTSS( - ctx context.Context, - keyGen observertypes.Keygen, - tssServer tss.TssServer, - zetacoreClient interfaces.ZetacoreClient, - keygenLogger zerolog.Logger, -) (string, error) { - keygenLogger.Info().Msgf("Keygen at blocknum %d , TSS signers %s ", keyGen.BlockNumber, keyGen.GranteePubkeys) - req := keygen.NewRequest(keyGen.GranteePubkeys, keyGen.BlockNumber, "0.14.0", tsscommon.ECDSA) - res, err := tssServer.Keygen(req) - if res.Status != tsscommon.Success || res.PubKey == "" { - keygenLogger.Error().Msgf("keygen fail: reason %s blame nodes %s", res.Blame.FailReason, res.Blame.BlameNodes) - // Need to broadcast keygen blame result here - digest, err := digestReq(req) - if err != nil { - return "", err - } - index := fmt.Sprintf("keygen-%s-%d", digest, keyGen.BlockNumber) - zetaHash, err := zetacoreClient.PostVoteBlameData( - ctx, - &res.Blame, - zetacoreClient.Chain().ChainId, - index, - ) - if err != nil { - keygenLogger.Error().Err(err).Msg("error sending blame data to core") - return "", err - } - - // Increment Blame counter - for _, node := range res.Blame.BlameNodes { - metrics.TssNodeBlamePerPubKey.WithLabelValues(node.Pubkey).Inc() - } - - keygenLogger.Info().Msgf("keygen posted blame data tx hash: %s", zetaHash) - return "", fmt.Errorf("keygen fail: reason %s blame nodes %s", res.Blame.FailReason, res.Blame.BlameNodes) - } - if err != nil { - keygenLogger.Error().Msgf("keygen fail: reason %s ", err.Error()) - return "", err - } - // Keygen succeed - keygenLogger.Info().Msgf("Keygen success! keygen response: %v", res) - return res.PubKey, nil -} - -// TestTSS tests the TSS keygen by signing a sample message with the TSS key. -func TestTSS(pubkey string, tssServer tss.TssServer, logger zerolog.Logger) error { - keygenLogger := logger.With().Str(logs.FieldModule, "tss_test_keygen").Logger() - keygenLogger.Info().Msgf("KeyGen success ! Doing a Key-sign test") - // KeySign can fail even if TSS keygen is successful, just logging the error here to break out of outer loop and report TSS - err := TestKeysign(pubkey, tssServer) - if err != nil { - return err - } - return nil -} - -func digestReq(request keygen.Request) (string, error) { - bytes, err := json.Marshal(request) - if err != nil { - return "", err - } - - hasher := sha3.NewLegacyKeccak256() - hasher.Write(bytes) - digest := hex.EncodeToString(hasher.Sum(nil)) - - return digest, nil -} diff --git a/zetaclient/tss/keygen.go b/zetaclient/tss/keygen.go new file mode 100644 index 0000000000..ae5067445e --- /dev/null +++ b/zetaclient/tss/keygen.go @@ -0,0 +1,231 @@ +package tss + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "time" + + "github.com/pkg/errors" + "github.com/rs/zerolog" + tsscommon "gitlab.com/thorchain/tss/go-tss/common" + "gitlab.com/thorchain/tss/go-tss/keygen" + "gitlab.com/thorchain/tss/go-tss/tss" + "golang.org/x/crypto/sha3" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/ticker" + observertypes "github.com/zeta-chain/node/x/observer/types" + "github.com/zeta-chain/node/zetaclient/logs" + "github.com/zeta-chain/node/zetaclient/metrics" + "github.com/zeta-chain/node/zetaclient/zetacore" +) + +const ( + receiveSuccess = chains.ReceiveStatus_success + receiveFailed = chains.ReceiveStatus_failed +) + +type keygenCeremony struct { + tss *tss.TssServer + zetacore *zetacore.Client + lastSeenBlock int64 + logger zerolog.Logger +} + +// KeygenCeremony runs TSS keygen ceremony as a blocking thread. +// Most likely the keygen is already generated, so this function will be a noop. +func KeygenCeremony(ctx context.Context, tssServer *tss.TssServer, zc *zetacore.Client, logger zerolog.Logger) error { + const interval = time.Second + + ceremony := keygenCeremony{ + tss: tssServer, + zetacore: zc, + logger: logger.With().Str(logs.FieldModule, "tss_keygen").Logger(), + } + + task := func(ctx context.Context, t *ticker.Ticker) error { + shouldRetry, err := ceremony.iteration(ctx) + switch { + case shouldRetry: + if err != nil { + logger.Error().Err(err).Msg("Keygen error. Retrying...") + } + + // continue the ticker + return nil + case err != nil: + return errors.Wrap(err, "keygen ceremony failed") + default: + // keygen ceremony is complete (or noop) + t.Stop() + return nil + } + } + + return ticker.Run(ctx, interval, task, ticker.WithLogger(logger, "tss_keygen")) +} + +// iteration runs ceremony iteration every time interval. +// - Get the keygen task from zetacore +// - If the keygen is already generated, return (false, nil) => ceremony is complete +// - If the keygen is pending, ensure we're on the right block +// - Iteration also ensured that the logic is invoked ONLY once per block (regardless of the interval) +func (k *keygenCeremony) iteration(ctx context.Context) (shouldRetry bool, err error) { + keygenTask, err := k.zetacore.GetKeyGen(ctx) + switch { + case err != nil: + return true, errors.Wrap(err, "unable to get keygen via RPC") + case keygenTask.Status == observertypes.KeygenStatus_KeyGenSuccess: + // all good, tss key is already generated + return false, nil + case keygenTask.Status == observertypes.KeygenStatus_KeyGenFailed: + // come back later to try again (zetacore will make status=pending) + return true, nil + case keygenTask.Status == observertypes.KeygenStatus_PendingKeygen: + // okay, let's try to generate the TSS key + default: + return false, fmt.Errorf("unexpected keygen status %q", keygenTask.Status.String()) + } + + keygenHeight := keygenTask.BlockNumber + + zetaHeight, err := k.zetacore.GetBlockHeight(ctx) + switch { + case err != nil: + return true, errors.Wrap(err, "unable to get zeta height") + case k.blockThrottled(zetaHeight): + return true, nil + case zetaHeight < keygenHeight: + k.logger.Info(). + Int64("keygen.height", keygenHeight). + Int64("zeta_height", zetaHeight). + Msgf("Waiting for keygen block to arrive or new keygen block to be set") + return true, nil + case zetaHeight > keygenHeight: + k.logger.Info(). + Int64("keygen.height", keygenHeight). + Int64("zeta_height", zetaHeight). + Msgf("Waiting for keygen finalization") + return true, nil + } + + // Now we know that the keygen status is PENDING, and we are the KEYGEN block. + // Let's perform TSS Keygen and then post successful/failed vote to zetacore + newPubKey, err := k.performKeygen(ctx, keygenTask) + if err != nil { + k.logger.Error().Err(err).Msg("Keygen failed. Broadcasting failed TSS vote") + + // Vote for failure + failedVoteHash, err := k.zetacore.PostVoteTSS(ctx, "", keygenTask.BlockNumber, receiveFailed) + if err != nil { + return false, errors.Wrap(err, "failed to broadcast failed TSS vote") + } + + k.logger.Info(). + Str("keygen.failed_vote_tx_hash", failedVoteHash). + Msg("Broadcasted failed TSS keygen vote") + + return true, nil + } + + successVoteHash, err := k.zetacore.PostVoteTSS(ctx, newPubKey, keygenTask.BlockNumber, receiveSuccess) + if err != nil { + return false, errors.Wrap(err, "failed to broadcast successful TSS vote") + } + + k.logger.Info(). + Str("keygen.success_vote_tx_hash", successVoteHash). + Msg("Broadcasted successful TSS keygen vote") + + k.logger.Info().Msg("Performing TSS key-sign test") + + if err = TestKeysign(newPubKey, *k.tss); err != nil { + k.logger.Error().Err(err).Msg("Failed to test TSS keygen") + // signing can fail even if tss keygen is successful + } + + return false, nil +} + +// performKeygen performs TSS keygen flow via go-tss server. Returns the new TSS public key or error. +// If fails, then it will post blame data to zetacore and return an error. +func (k *keygenCeremony) performKeygen(ctx context.Context, keygenTask observertypes.Keygen) (string, error) { + k.logger.Warn(). + Int64("keygen.block", keygenTask.BlockNumber). + Strs("keygen.tss_signers", keygenTask.GranteePubkeys). + Msg("Performing a keygen!") + + req := keygen.NewRequest(keygenTask.GranteePubkeys, keygenTask.BlockNumber, Version, Algo) + + res, err := k.tss.Keygen(req) + switch { + case err != nil: + // returns error on network failure or other non-recoverable errors + // if the keygen is unsuccessful, the error will be nil + return "", errors.Wrap(err, "unable to perform keygen") + case res.Status == tsscommon.Success && res.PubKey != "": + // desired outcome + k.logger.Info(). + Interface("keygen.response", res). + Interface("keygen.tss_public_key", res.PubKey). + Msg("Keygen successfully generated!") + return res.PubKey, nil + } + + // Something went wrong, let's post blame results and then FAIL + k.logger.Error(). + Str("keygen.blame_round", res.Blame.Round). + Str("keygen.fail_reason", res.Blame.FailReason). + Interface("keygen.blame_nodes", res.Blame.BlameNodes). + Msg("Keygen failed! Sending blame data to zetacore") + + // increment blame counter + for _, node := range res.Blame.BlameNodes { + metrics.TssNodeBlamePerPubKey.WithLabelValues(node.Pubkey).Inc() + } + + blameDigest, err := digestReq(req) + if err != nil { + return "", errors.Wrap(err, "unable to create digest") + } + + blameIndex := fmt.Sprintf("keygen-%s-%d", blameDigest, keygenTask.BlockNumber) + chainID := k.zetacore.Chain().ChainId + + zetaHash, err := k.zetacore.PostVoteBlameData(ctx, &res.Blame, chainID, blameIndex) + if err != nil { + return "", errors.Wrap(err, "unable to post blame data to zetacore") + } + + k.logger.Info().Str("keygen.blame_tx_hash", zetaHash).Msg("Posted blame data to zetacore") + + return "", errors.Errorf("keygen failed: %s", res.Blame.FailReason) +} + +// returns true if the block is throttled i.e. we should wait for the next block. +func (k *keygenCeremony) blockThrottled(currentBlock int64) bool { + switch { + case currentBlock == 0: + return false + case k.lastSeenBlock == currentBlock: + return true + default: + k.lastSeenBlock = currentBlock + return false + } +} + +func digestReq(req keygen.Request) (string, error) { + bytes, err := json.Marshal(req) + if err != nil { + return "", err + } + + hasher := sha3.NewLegacyKeccak256() + hasher.Write(bytes) + digest := hex.EncodeToString(hasher.Sum(nil)) + + return digest, nil +} diff --git a/zetaclient/tss/tss_signer.go b/zetaclient/tss/tss_signer.go index bb887d9380..40ed1a2e66 100644 --- a/zetaclient/tss/tss_signer.go +++ b/zetaclient/tss/tss_signer.go @@ -594,7 +594,7 @@ func TestKeysign(tssPubkey string, tssServer tss.TssServer) error { []string{base64.StdEncoding.EncodeToString(H.Bytes())}, 10, nil, - "0.14.0", + Version, ) ksRes, err := tssServer.KeySign(keysignReq) if err != nil { From cdd27a8d362205573cc3fb0a2138f69090ccf0a0 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 15 Nov 2024 20:12:47 +0100 Subject: [PATCH 03/37] Refactor tss key sign test; streamline sig verification --- cmd/zetaclientd/start.go | 4 +- zetaclient/tss/config.go | 1 + zetaclient/tss/keygen.go | 2 +- zetaclient/tss/keysign.go | 119 +++++++++++++++++++++++++++++++++++ zetaclient/tss/tss_signer.go | 112 ++------------------------------- 5 files changed, 127 insertions(+), 111 deletions(-) create mode 100644 zetaclient/tss/keysign.go diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index b8cc72d23f..9c028744cd 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -271,9 +271,7 @@ func Start(_ *cobra.Command, _ []string) error { return err } if cfg.TestTssKeysign { - startLogger.Info().Msg("Performing TSS key-sign test") - - if err = mc.TestKeysign(tss.CurrentPubkey, *tss.Server); err != nil { + if err = mc.TestKeySign(tss.Server, tss.CurrentPubkey, startLogger); err != nil { startLogger.Error().Err(err). Str("tss.public_key", tss.CurrentPubkey). Msg("TSS key-sign failed") diff --git a/zetaclient/tss/config.go b/zetaclient/tss/config.go index dffcb17b51..ae965fd18f 100644 --- a/zetaclient/tss/config.go +++ b/zetaclient/tss/config.go @@ -12,6 +12,7 @@ import ( ) const ( + Port = 6668 Version = "0.14.0" Algo = tsscommon.ECDSA ) diff --git a/zetaclient/tss/keygen.go b/zetaclient/tss/keygen.go index ae5067445e..a70c7bb44a 100644 --- a/zetaclient/tss/keygen.go +++ b/zetaclient/tss/keygen.go @@ -141,7 +141,7 @@ func (k *keygenCeremony) iteration(ctx context.Context) (shouldRetry bool, err e k.logger.Info().Msg("Performing TSS key-sign test") - if err = TestKeysign(newPubKey, *k.tss); err != nil { + if err = TestKeySign(k.tss, newPubKey, k.logger); err != nil { k.logger.Error().Err(err).Msg("Failed to test TSS keygen") // signing can fail even if tss keygen is successful } diff --git a/zetaclient/tss/keysign.go b/zetaclient/tss/keysign.go new file mode 100644 index 0000000000..b10918a96f --- /dev/null +++ b/zetaclient/tss/keysign.go @@ -0,0 +1,119 @@ +package tss + +import ( + "bytes" + "encoding/base64" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/pkg/errors" + "github.com/rs/zerolog" + tsscommon "gitlab.com/thorchain/tss/go-tss/common" + "gitlab.com/thorchain/tss/go-tss/keysign" + "gitlab.com/thorchain/tss/go-tss/tss" + + "github.com/zeta-chain/node/pkg/cosmos" + "github.com/zeta-chain/node/zetaclient/logs" +) + +var ( + testKeySignData = []byte("hello meta") + base64Decode = base64.StdEncoding.Decode + base64DecodeString = base64.StdEncoding.DecodeString +) + +// TestKeySign performs a TSS key-sign test of sample data. +func TestKeySign(tssServer *tss.TssServer, tssPubKey string, logger zerolog.Logger) error { + logger = logger.With().Str(logs.FieldModule, "tss_keysign").Logger() + + hashedData := crypto.Keccak256Hash(testKeySignData) + + logger.Info(). + Str("keysign.test_data", string(testKeySignData)). + Str("keysign.test_data_hash", hashedData.String()). + Msg("Performing TSS key-sign test") + + req := keysign.NewRequest( + tssPubKey, + []string{base64.StdEncoding.EncodeToString(hashedData.Bytes())}, + 10, + nil, + Version, + ) + + res, err := tssServer.KeySign(req) + switch { + case err != nil: + return errors.Wrap(err, "key signing request error") + case res.Status != tsscommon.Success: + logger.Error().Interface("keysign.fail_blame", res.Blame).Msg("Keysign failed") + return errors.Wrapf(err, "key signing is not successful (status %d)", res.Status) + case len(res.Signatures) == 0: + return errors.New("signatures list is empty") + } + + // 32B msg hash, 32B R, 32B S, 1B RC + signature := res.Signatures[0] + + logger.Info().Interface("keysign.signature", signature).Msg("Received signature from TSS") + + if _, err = VerifySignature(signature, tssPubKey, hashedData.Bytes()); err != nil { + return errors.Wrap(err, "signature verification failed") + } + + logger.Info().Msg("TSS key-sign test passed") + + return nil +} + +// VerifySignature checks that keysign.Signature is valid and origins from expected TSS public key. +// Also returns signature as [65]byte (R, S, V) +func VerifySignature(sig keysign.Signature, tssPubKey string, expectedMsgHash []byte) ([65]byte, error) { + // Check that msg hash equals msg hash in the signature + actualMsgHash, err := base64DecodeString(sig.Msg) + switch { + case err != nil: + return [65]byte{}, errors.Wrap(err, "unable to decode message hash") + case !bytes.Equal(expectedMsgHash, actualMsgHash): + return [65]byte{}, errors.New("message hash mismatch") + } + + // Prepare expected public key + expectedPubKey, err := cosmos.GetPubKeyFromBech32(cosmos.Bech32PubKeyTypeAccPub, tssPubKey) + if err != nil { + return [65]byte{}, errors.Wrap(err, "unable to decode tss pub key from bech32") + } + + sigBytes, err := SignatureToBytes(sig) + if err != nil { + return [65]byte{}, errors.Wrap(err, "unable to convert signature to bytes") + } + + // Recover public key from signature + actualPubKey, err := crypto.SigToPub(expectedMsgHash, sigBytes[:]) + if err != nil { + return [65]byte{}, errors.Wrap(err, "unable to recover public key from signature") + } + + if !bytes.Equal(expectedPubKey.Bytes(), crypto.CompressPubkey(actualPubKey)) { + return [65]byte{}, errors.New("public key mismatch") + } + + return sigBytes, nil +} + +// SignatureToBytes converts keysign.Signature to [65]byte (R, S, V) +func SignatureToBytes(input keysign.Signature) (sig [65]byte, err error) { + if _, err = base64Decode(sig[:32], []byte(input.R)); err != nil { + return sig, errors.Wrap(err, "unable to decode R") + } + + if _, err = base64Decode(sig[32:64], []byte(input.S)); err != nil { + return sig, errors.Wrap(err, "unable to decode S") + } + + if _, err = base64Decode(sig[64:65], []byte(input.RecoveryID)); err != nil { + return sig, errors.Wrap(err, "unable to decode RecoveryID (V)") + } + + return sig, nil +} diff --git a/zetaclient/tss/tss_signer.go b/zetaclient/tss/tss_signer.go index 40ed1a2e66..bcad6f7f8d 100644 --- a/zetaclient/tss/tss_signer.go +++ b/zetaclient/tss/tss_signer.go @@ -174,7 +174,7 @@ func SetupTSSServer( tssServer, err := tss.NewTss( bootstrapPeers, - 6668, + Port, privkey, tsspath, thorcommon.TssConfig{ @@ -241,7 +241,7 @@ func (tss *TSS) Sign( []string{base64.StdEncoding.EncodeToString(H)}, int64(height), nil, - "0.14.0", + Version, ) end := tss.KeysignsTracker.StartMsgSign() ksRes, err := tss.Server.KeySign(keysignReq) @@ -280,30 +280,12 @@ func (tss *TSS) Sign( return [65]byte{}, fmt.Errorf("keysign fail: signature list is empty") } - if !verifySignature(tssPubkey, signature, H) { - return [65]byte{}, fmt.Errorf("signuature verification failue") - } - - var sigbyte [65]byte - _, err = base64.StdEncoding.Decode(sigbyte[:32], []byte(signature[0].R)) - if err != nil { - log.Error().Err(err).Msg("decoding signature R") - return [65]byte{}, fmt.Errorf("signuature verification failure (R) %w", err) - } - - _, err = base64.StdEncoding.Decode(sigbyte[32:64], []byte(signature[0].S)) - if err != nil { - log.Error().Err(err).Msg("decoding signature S") - return [65]byte{}, fmt.Errorf("signuature verification failue (S): %w", err) - } - - _, err = base64.StdEncoding.Decode(sigbyte[64:65], []byte(signature[0].RecoveryID)) + sig, err := VerifySignature(signature[0], tssPubkey, H) if err != nil { - log.Error().Err(err).Msg("decoding signature RecoveryID") - return [65]byte{}, fmt.Errorf("signuature verification failue (V) %w", err) + return [65]byte{}, fmt.Errorf("unable to verify signature: %w", err) } - return sigbyte, nil + return sig, nil } // SignBatch is hash of some data @@ -579,95 +561,11 @@ func GetTssAddrEVM(tssPubkey string) (ethcommon.Address, error) { return keyAddr, nil } -// TestKeysign tests the keysign -// it is called when a new TSS is generated to ensure the network works as expected -// TODO(revamp): move to a test package - -func TestKeysign(tssPubkey string, tssServer tss.TssServer) error { - log.Info().Msg("trying keysign...") - data := []byte("hello meta") - H := crypto.Keccak256Hash(data) - log.Info().Msgf("hash of data (hello meta) is %s", H) - - keysignReq := keysign.NewRequest( - tssPubkey, - []string{base64.StdEncoding.EncodeToString(H.Bytes())}, - 10, - nil, - Version, - ) - ksRes, err := tssServer.KeySign(keysignReq) - if err != nil { - log.Warn().Msg("keysign fail") - } - - signature := ksRes.Signatures - // [{cyP8i/UuCVfQKDsLr1kpg09/CeIHje1FU6GhfmyMD5Q= D4jXTH3/CSgCg+9kLjhhfnNo3ggy9DTQSlloe3bbKAs= eY++Z2LwsuKG1JcghChrsEJ4u9grLloaaFZNtXI3Ujk= AA==}] - // 32B msg hash, 32B R, 32B S, 1B RC - log.Info().Msgf("signature of helloworld... %v", signature) - - if len(signature) == 0 { - log.Info().Msgf("signature has length 0, skipping verify") - return fmt.Errorf("signature has length 0") - } - - verifySignature(tssPubkey, signature, H.Bytes()) - if verifySignature(tssPubkey, signature, H.Bytes()) { - return nil - } - - return fmt.Errorf("verify signature fail") -} - -// IsEnvFlagEnabled checks if the environment flag is enabled func IsEnvFlagEnabled(flag string) bool { value := os.Getenv(flag) return value == "true" || value == "1" } -// verifySignature verifies the signature -// TODO(revamp): move to a test package -func verifySignature(tssPubkey string, signature []keysign.Signature, H []byte) bool { - if len(signature) == 0 { - log.Warn().Msg("verify_signature: empty signature array") - return false - } - pubkey, err := cosmos.GetPubKeyFromBech32(cosmos.Bech32PubKeyTypeAccPub, tssPubkey) - if err != nil { - log.Error().Msg("get pubkey from bech32 fail") - } - - // verify the signature of msg. - var sigbyte [65]byte - _, err = base64.StdEncoding.Decode(sigbyte[:32], []byte(signature[0].R)) - if err != nil { - log.Error().Err(err).Msg("decoding signature R") - return false - } - - _, err = base64.StdEncoding.Decode(sigbyte[32:64], []byte(signature[0].S)) - if err != nil { - log.Error().Err(err).Msg("decoding signature S") - return false - } - - _, err = base64.StdEncoding.Decode(sigbyte[64:65], []byte(signature[0].RecoveryID)) - if err != nil { - log.Error().Err(err).Msg("decoding signature RecoveryID") - return false - } - - sigPublicKey, err := crypto.SigToPub(H, sigbyte[:]) - if err != nil { - log.Error().Err(err).Msg("SigToPub error in verify_signature") - return false - } - - compressedPubkey := crypto.CompressPubkey(sigPublicKey) - log.Info().Msgf("pubkey %s recovered pubkey %s", pubkey.String(), hex.EncodeToString(compressedPubkey)) - return bytes.Equal(pubkey.Bytes(), compressedPubkey) -} - // combineDigests combines the digests func combineDigests(digestList []string) []byte { digestConcat := strings.Join(digestList[:], "") From 39db08ab467d7ba6ec29f49e202b566f53b241a2 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:22:00 +0100 Subject: [PATCH 04/37] Remove optional pub key (tss) --- .../bitcoin/signer/signer_keysign_test.go | 2 +- zetaclient/chains/evm/signer/signer.go | 2 +- zetaclient/chains/interfaces/interfaces.go | 20 +++---------------- zetaclient/chains/solana/signer/whitelist.go | 2 +- zetaclient/chains/solana/signer/withdraw.go | 2 +- .../chains/solana/signer/withdraw_spl.go | 2 +- .../chains/ton/observer/observer_test.go | 2 +- zetaclient/chains/ton/signer/signer.go | 2 +- zetaclient/chains/ton/signer/signer_test.go | 2 +- zetaclient/testutils/mocks/tss_signer.go | 2 +- zetaclient/tss/tss_signer.go | 13 +----------- 11 files changed, 13 insertions(+), 38 deletions(-) diff --git a/zetaclient/chains/bitcoin/signer/signer_keysign_test.go b/zetaclient/chains/bitcoin/signer/signer_keysign_test.go index a339b051dd..e21e468649 100644 --- a/zetaclient/chains/bitcoin/signer/signer_keysign_test.go +++ b/zetaclient/chains/bitcoin/signer/signer_keysign_test.go @@ -148,7 +148,7 @@ func getTSSTX( return "", err } - sig65B, err := tss.Sign(ctx, witnessHash, 10, 10, 0, "") + sig65B, err := tss.Sign(ctx, witnessHash, 10, 10, 0) R := &btcec.ModNScalar{} R.SetBytes((*[32]byte)(sig65B[:32])) S := &btcec.ModNScalar{} diff --git a/zetaclient/chains/evm/signer/signer.go b/zetaclient/chains/evm/signer/signer.go index 5cebb323c1..e666223813 100644 --- a/zetaclient/chains/evm/signer/signer.go +++ b/zetaclient/chains/evm/signer/signer.go @@ -197,7 +197,7 @@ func (signer *Signer) Sign( hashBytes := signer.ethSigner.Hash(tx).Bytes() - sig, err := signer.TSS().Sign(ctx, hashBytes, height, nonce, signer.Chain().ChainId, "") + sig, err := signer.TSS().Sign(ctx, hashBytes, height, nonce, signer.Chain().ChainId) if err != nil { return nil, nil, nil, err } diff --git a/zetaclient/chains/interfaces/interfaces.go b/zetaclient/chains/interfaces/interfaces.go index 4ed7c0797e..ded4e0f122 100644 --- a/zetaclient/chains/interfaces/interfaces.go +++ b/zetaclient/chains/interfaces/interfaces.go @@ -227,25 +227,11 @@ type EVMJSONRPCClient interface { // TSSSigner is the interface for TSS signer type TSSSigner interface { Pubkey() []byte - - // Sign signs the data - // Note: it specifies optionalPubkey to use a different pubkey than the current pubkey set during keygen - // TODO: check if optionalPubkey is needed - // https://github.com/zeta-chain/node/issues/2085 - Sign( - ctx context.Context, - data []byte, - height uint64, - nonce uint64, - chainID int64, - optionalPubkey string, - ) ([65]byte, error) - - // SignBatch signs the data in batch - SignBatch(ctx context.Context, digests [][]byte, height uint64, nonce uint64, chainID int64) ([][65]byte, error) - EVMAddress() ethcommon.Address EVMAddressList() []ethcommon.Address BTCAddress(chainID int64) (*btcutil.AddressWitnessPubKeyHash, error) PubKeyCompressedBytes() []byte + + Sign(ctx context.Context, data []byte, height, nonce uint64, chainID int64) ([65]byte, error) + SignBatch(ctx context.Context, digests [][]byte, height, nonce uint64, chainID int64) ([][65]byte, error) } diff --git a/zetaclient/chains/solana/signer/whitelist.go b/zetaclient/chains/solana/signer/whitelist.go index 6d9055adc7..435a879275 100644 --- a/zetaclient/chains/solana/signer/whitelist.go +++ b/zetaclient/chains/solana/signer/whitelist.go @@ -31,7 +31,7 @@ func (signer *Signer) createAndSignMsgWhitelist( // sign the message with TSS to get an ECDSA signature. // the produced signature is in the [R || S || V] format where V is 0 or 1. - signature, err := signer.TSS().Sign(ctx, msgHash[:], height, nonce, chain.ChainId, "") + signature, err := signer.TSS().Sign(ctx, msgHash[:], height, nonce, chain.ChainId) if err != nil { return nil, errors.Wrap(err, "Key-sign failed") } diff --git a/zetaclient/chains/solana/signer/withdraw.go b/zetaclient/chains/solana/signer/withdraw.go index 5a27095f6f..d8b2ab0997 100644 --- a/zetaclient/chains/solana/signer/withdraw.go +++ b/zetaclient/chains/solana/signer/withdraw.go @@ -43,7 +43,7 @@ func (signer *Signer) createAndSignMsgWithdraw( // sign the message with TSS to get an ECDSA signature. // the produced signature is in the [R || S || V] format where V is 0 or 1. - signature, err := signer.TSS().Sign(ctx, msgHash[:], height, nonce, chain.ChainId, "") + signature, err := signer.TSS().Sign(ctx, msgHash[:], height, nonce, chain.ChainId) if err != nil { return nil, errors.Wrap(err, "Key-sign failed") } diff --git a/zetaclient/chains/solana/signer/withdraw_spl.go b/zetaclient/chains/solana/signer/withdraw_spl.go index bf03260eca..edb3b649de 100644 --- a/zetaclient/chains/solana/signer/withdraw_spl.go +++ b/zetaclient/chains/solana/signer/withdraw_spl.go @@ -57,7 +57,7 @@ func (signer *Signer) createAndSignMsgWithdrawSPL( // sign the message with TSS to get an ECDSA signature. // the produced signature is in the [R || S || V] format where V is 0 or 1. - signature, err := signer.TSS().Sign(ctx, msgHash[:], height, nonce, chain.ChainId, "") + signature, err := signer.TSS().Sign(ctx, msgHash[:], height, nonce, chain.ChainId) if err != nil { return nil, errors.Wrap(err, "Key-sign failed") } diff --git a/zetaclient/chains/ton/observer/observer_test.go b/zetaclient/chains/ton/observer/observer_test.go index 45a79f65ec..6fc8242e1f 100644 --- a/zetaclient/chains/ton/observer/observer_test.go +++ b/zetaclient/chains/ton/observer/observer_test.go @@ -199,7 +199,7 @@ func (ts *testSuite) sign(msg signable) { hash, err := msg.Hash() require.NoError(ts.t, err) - sig, err := ts.tss.Sign(ts.ctx, hash[:], 0, 0, 0, "") + sig, err := ts.tss.Sign(ts.ctx, hash[:], 0, 0, 0) require.NoError(ts.t, err) msg.SetSignature(sig) diff --git a/zetaclient/chains/ton/signer/signer.go b/zetaclient/chains/ton/signer/signer.go index 689692eece..fe8e7c50e3 100644 --- a/zetaclient/chains/ton/signer/signer.go +++ b/zetaclient/chains/ton/signer/signer.go @@ -187,7 +187,7 @@ func (s *Signer) SignMessage(ctx context.Context, msg Signable, zetaHeight, nonc chainID := s.Chain().ChainId // sig = [65]byte {R, S, V (recovery ID)} - sig, err := s.TSS().Sign(ctx, hash[:], zetaHeight, nonce, chainID, "") + sig, err := s.TSS().Sign(ctx, hash[:], zetaHeight, nonce, chainID) if err != nil { return errors.Wrap(err, "unable to sign the message") } diff --git a/zetaclient/chains/ton/signer/signer_test.go b/zetaclient/chains/ton/signer/signer_test.go index f491b59391..9f3a5b7c63 100644 --- a/zetaclient/chains/ton/signer/signer_test.go +++ b/zetaclient/chains/ton/signer/signer_test.go @@ -201,7 +201,7 @@ func (ts *testSuite) Sign(msg Signable) { hash, err := msg.Hash() require.NoError(ts.t, err) - sig, err := ts.tss.Sign(ts.ctx, hash[:], 0, 0, 0, "") + sig, err := ts.tss.Sign(ts.ctx, hash[:], 0, 0, 0) require.NoError(ts.t, err) msg.SetSignature(sig) diff --git a/zetaclient/testutils/mocks/tss_signer.go b/zetaclient/testutils/mocks/tss_signer.go index 3fa003a3c0..89868fc1dd 100644 --- a/zetaclient/testutils/mocks/tss_signer.go +++ b/zetaclient/testutils/mocks/tss_signer.go @@ -103,7 +103,7 @@ func (s *TSS) WithPrivKey(privKey *ecdsa.PrivateKey) *TSS { } // Sign uses test key unrelated to any tss key in production -func (s *TSS) Sign(_ context.Context, data []byte, _ uint64, _ uint64, _ int64, _ string) ([65]byte, error) { +func (s *TSS) Sign(_ context.Context, data []byte, _ uint64, _ uint64, _ int64) ([65]byte, error) { // return error if tss is paused if s.paused { return [65]byte{}, fmt.Errorf("tss is paused") diff --git a/zetaclient/tss/tss_signer.go b/zetaclient/tss/tss_signer.go index bcad6f7f8d..2f0d11aa3a 100644 --- a/zetaclient/tss/tss_signer.go +++ b/zetaclient/tss/tss_signer.go @@ -218,22 +218,11 @@ func (tss *TSS) Pubkey() []byte { // Sign signs a digest // digest should be Hashes of some data -// NOTE: Specify optionalPubkey to use a different pubkey than the current pubkey set during keygen -func (tss *TSS) Sign( - ctx context.Context, - digest []byte, - height uint64, - nonce uint64, - chainID int64, - optionalPubKey string, -) ([65]byte, error) { +func (tss *TSS) Sign(ctx context.Context, digest []byte, height, nonce uint64, chainID int64) ([65]byte, error) { H := digest log.Debug().Msgf("hash of digest is %s", H) tssPubkey := tss.CurrentPubkey - if optionalPubKey != "" { - tssPubkey = optionalPubKey - } // #nosec G115 always in range keysignReq := keysign.NewRequest( From e1109fdb6daa9848e938d0fe094f21f4c5c15da4 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:44:45 +0100 Subject: [PATCH 05/37] Implement PubKey entity --- zetaclient/tss/pubkey.go | 95 +++++++++++++++++++++++++++++++++++ zetaclient/tss/pubkey_test.go | 40 +++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 zetaclient/tss/pubkey.go create mode 100644 zetaclient/tss/pubkey_test.go diff --git a/zetaclient/tss/pubkey.go b/zetaclient/tss/pubkey.go new file mode 100644 index 0000000000..5bd15972a1 --- /dev/null +++ b/zetaclient/tss/pubkey.go @@ -0,0 +1,95 @@ +package tss + +import ( + "crypto/ecdsa" + "crypto/elliptic" + + "github.com/btcsuite/btcd/btcutil" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + eth "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/pkg/errors" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/cosmos" +) + +// PubKey represents TSS public key in various formats. +type PubKey struct { + cosmosPubKey cryptotypes.PubKey + ecdsaPubKey *ecdsa.PublicKey +} + +// NewPubKeyFromBech32 creates a new PubKey from a bech32 address. +// Example: `zetapub1addwnpepq2fdhcmfyv07s86djjca835l4f2n2ta0c7le6vnl508mseca2s9g6slj0gm` +func NewPubKeyFromBech32(bech32 string) (PubKey, error) { + if bech32 == "" { + return PubKey{}, errors.New("empty bech32 address") + } + + cosmosPubKey, err := cosmos.GetPubKeyFromBech32(cosmos.Bech32PubKeyTypeAccPub, bech32) + if err != nil { + return PubKey{}, errors.Wrap(err, "unable to GetPubKeyFromBech32") + } + + pubKey, err := crypto.DecompressPubkey(cosmosPubKey.Bytes()) + if err != nil { + return PubKey{}, errors.Wrap(err, "unable to DecompressPubkey") + } + + crypto.FromECDSAPub(pubKey) + + return PubKey{ + cosmosPubKey: cosmosPubKey, + ecdsaPubKey: pubKey, + }, nil +} + +// Bytes marshals pubKey to bytes either as compressed or uncompressed slice. +// +// In ECDSA, a compressed pubKey includes only the X and a parity bit for the Y, +// allowing the full Y to be reconstructed using the elliptic curve equation, +// thus reducing the key size while maintaining the ability to fully recover the pubKey. +func (k PubKey) Bytes(compress bool) []byte { + pk := k.ecdsaPubKey + if compress { + return elliptic.MarshalCompressed(pk.Curve, pk.X, pk.Y) + } + + return crypto.FromECDSAPub(pk) +} + +// Bech32String returns the bech32 string of the public key. +// Example: `zetapub1addwnpepq2fdhcmfyv07s86djjca835l4f2n2ta0c7le6vnl508mseca2s9g6slj0gm` +func (k PubKey) Bech32String() string { + v, err := cosmos.Bech32ifyPubKey(cosmos.Bech32PubKeyTypeAccPub, k.cosmosPubKey) + + // should not happen as we only set k.cosmosPubKey from the constructor + if err != nil { + panic("PubKey.Bech32String: " + err.Error()) + } + + return v +} + +// AddressBTC returns the bitcoin address of the public key. +func (k PubKey) AddressBTC(chainID int64) (*btcutil.AddressWitnessPubKeyHash, error) { + return bitcoinP2WPKH(k.Bytes(true), chainID) +} + +// AddressEVM returns the ethereum address of the public key. +func (k PubKey) AddressEVM() eth.Address { + return crypto.PubkeyToAddress(*k.ecdsaPubKey) +} + +// bitcoinP2WPKH returns P2WPKH (pay to witness pub key hash) address from the compressed pub key. +func bitcoinP2WPKH(pkCompressed []byte, chainID int64) (*btcutil.AddressWitnessPubKeyHash, error) { + params, err := chains.BitcoinNetParamsFromChainID(chainID) + if err != nil { + return nil, errors.Wrap(err, "unable to get btc net params") + } + + hash := btcutil.Hash160(pkCompressed) + + return btcutil.NewAddressWitnessPubKeyHash(hash, params) +} diff --git a/zetaclient/tss/pubkey_test.go b/zetaclient/tss/pubkey_test.go new file mode 100644 index 0000000000..4fc493150e --- /dev/null +++ b/zetaclient/tss/pubkey_test.go @@ -0,0 +1,40 @@ +package tss + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/cmd" + "github.com/zeta-chain/node/pkg/chains" +) + +func TestPubKey(t *testing.T) { + cmd.SetupCosmosConfig() + + t.Run("Invalid", func(t *testing.T) { + _, err := NewPubKeyFromBech32("") + require.ErrorContains(t, err, "empty bech32 address") + }) + + t.Run("Valid", func(t *testing.T) { + // ARRANGE + const sample = `zetapub1addwnpepqtadxdyt037h86z60nl98t6zk56mw5zpnm79tsmvspln3hgt5phdc79kvfc` + + // ACT + pk, err := NewPubKeyFromBech32(sample) + + // ASSERT + require.NoError(t, err) + assert.NotEmpty(t, pk) + + addrEVM := pk.AddressEVM() + addrBTC, err := pk.AddressBTC(chains.BitcoinMainnet.ChainId) + require.NoError(t, err) + + assert.Equal(t, sample, pk.Bech32String()) + assert.Equal(t, "0x70e967acfcc17c3941e87562161406d41676fd83", strings.ToLower(addrEVM.Hex())) + assert.Equal(t, "bc1qm24wp577nk8aacckv8np465z3dvmu7ry45el6y", addrBTC.String()) + }) +} From 1190db95324800771a420c2a406eed4f244a5b87 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:36:11 +0100 Subject: [PATCH 06/37] Implement new TSS service layer. Add unit tests --- zetaclient/tss/pubkey.go | 2 - zetaclient/tss/service.go | 229 +++++++++++++++++++++++++++++++++ zetaclient/tss/service_test.go | 200 ++++++++++++++++++++++++++++ 3 files changed, 429 insertions(+), 2 deletions(-) create mode 100644 zetaclient/tss/service.go create mode 100644 zetaclient/tss/service_test.go diff --git a/zetaclient/tss/pubkey.go b/zetaclient/tss/pubkey.go index 5bd15972a1..bd7ba60a9e 100644 --- a/zetaclient/tss/pubkey.go +++ b/zetaclient/tss/pubkey.go @@ -37,8 +37,6 @@ func NewPubKeyFromBech32(bech32 string) (PubKey, error) { return PubKey{}, errors.Wrap(err, "unable to DecompressPubkey") } - crypto.FromECDSAPub(pubKey) - return PubKey{ cosmosPubKey: cosmosPubKey, ecdsaPubKey: pubKey, diff --git a/zetaclient/tss/service.go b/zetaclient/tss/service.go new file mode 100644 index 0000000000..b9b69f1718 --- /dev/null +++ b/zetaclient/tss/service.go @@ -0,0 +1,229 @@ +package tss + +import ( + "context" + "encoding/base64" + "encoding/hex" + "fmt" + "strings" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/pkg/errors" + "github.com/rs/zerolog" + thorcommon "gitlab.com/thorchain/tss/go-tss/common" + "gitlab.com/thorchain/tss/go-tss/keysign" + + observertypes "github.com/zeta-chain/node/x/observer/types" + "github.com/zeta-chain/node/zetaclient/chains/interfaces" + "github.com/zeta-chain/node/zetaclient/logs" +) + +// KeySigner signs messages using TSS (subset of go-tss) +type KeySigner interface { + KeySign(req keysign.Request) (keysign.Response, error) +} + +// Service TSS service +type Service struct { + zetacore interfaces.ZetacoreClient + tss KeySigner + currentPubKey PubKey + + postBlame bool + logger zerolog.Logger +} + +type serviceConfig struct { + postBlame bool +} + +// Opt Service option. +type Opt func(cfg *serviceConfig, logger zerolog.Logger) error + +// WithPostBlame configures the TSS service to post blame in case of failed key signatures. +func WithPostBlame(postBlame bool) Opt { + return func(cfg *serviceConfig, _ zerolog.Logger) error { + cfg.postBlame = postBlame + return nil + } +} + +// NewService Service constructor. +// TODO Constructor +// TODO PubKey struct +// TODO Test cases for bootstrap +// TODO metrics +// TODO LRU cache +func NewService( + keySigner KeySigner, + tssPubKeyBech32 string, + zc interfaces.ZetacoreClient, + logger zerolog.Logger, + opts ...Opt, +) (*Service, error) { + logger = logger.With().Str(logs.FieldModule, "tss_service").Logger() + + // Apply opts + var cfg serviceConfig + for _, opt := range opts { + if err := opt(&cfg, logger); err != nil { + return nil, errors.Wrap(err, "failed to apply tss config option") + } + } + + currentTSSPubKey, err := NewPubKeyFromBech32(tssPubKeyBech32) + if err != nil { + return nil, errors.Wrap(err, "invalid tss pub key") + } + + // todo metrics + + return &Service{ + tss: keySigner, + currentPubKey: currentTSSPubKey, + zetacore: zc, + postBlame: cfg.postBlame, + logger: logger, + }, nil +} + +// PubKey returns current TSS PubKey. +func (s *Service) PubKey() PubKey { + return s.currentPubKey +} + +// Sign signs msg digest (hash). Returns signature in the format of R (32B), S (32B), V (1B). +func (s *Service) Sign(ctx context.Context, digest []byte, height, nonce uint64, chainID int64) ([65]byte, error) { + sigs, err := s.SignBatch(ctx, [][]byte{digest}, height, nonce, chainID) + if err != nil { + return [65]byte{}, err + } + + return sigs[0], nil +} + +// SignBatch signs msgs digests (hash). Returns list of signatures in the format of R (32B), S (32B), V (1B). +func (s *Service) SignBatch( + ctx context.Context, + digests [][]byte, + height, nonce uint64, + chainID int64, +) ([][65]byte, error) { + if len(digests) == 0 { + return nil, errors.New("empty digests list") + } + + // todo check cache for digest & block height & chainID -> return signature (LRU cache) + + digestsBase64 := make([]string, len(digests)) + for i, digest := range digests { + digestsBase64[i] = base64.StdEncoding.EncodeToString(digest) + } + + tssPubKeyBech32 := s.PubKey().Bech32String() + + // #nosec G115 always in range + req := keysign.NewRequest( + tssPubKeyBech32, + digestsBase64, + int64(height), + nil, + Version, + ) + + res, err := s.sign(req) + switch { + case err != nil: + // unexpected error (not related to failed key sign) + return nil, errors.Wrap(err, "unable to perform a key sign") + case res.Status == thorcommon.Fail: + return nil, s.blameFailure(ctx, req, res, digests, height, nonce, chainID) + case res.Status != thorcommon.Success: + return nil, fmt.Errorf("keysign fail: status %d", res.Status) + case len(res.Signatures) == 0: + return nil, fmt.Errorf("keysign fail: signature list is empty") + case len(res.Signatures) != len(digests): + return nil, fmt.Errorf("keysign fail: signature list length mismatch") + } + + signatures := make([][65]byte, len(res.Signatures)) + for i, sigResponse := range res.Signatures { + signatures[i], err = VerifySignature(sigResponse, tssPubKeyBech32, digests[i]) + if err != nil { + return nil, fmt.Errorf("unable to verify signature: %w (#%d)", err, i) + } + } + + // todo sig save to LRU cache (chain-id + digest). We need LRU per EACH chain + + return signatures, nil +} + +func (s *Service) sign(req keysign.Request) (keysign.Response, error) { + // todo track signs (metrics) + res, err := s.tss.KeySign(req) + // todo finish tracking + + return res, err +} + +func (s *Service) blameFailure( + ctx context.Context, + req keysign.Request, + res keysign.Response, + digests [][]byte, + height uint64, + nonce uint64, + chainID int64, +) error { + errFailure := errors.Errorf("keysign failed: %s", res.Blame.FailReason) + lf := keysignLogFields(req, height, nonce, chainID) + + s.logger.Error().Err(errFailure). + Fields(lf). + Interface("keysign.fail_blame", res.Blame). + Msg("Keysign failed") + + // todo inc blame metrics + + if !s.postBlame { + return errFailure + } + + var digest []byte + if len(req.Messages) > 1 { + digest = combineDigests(req.Messages) + } else { + digest = digests[0] + } + + digestHex := hex.EncodeToString(digest) + index := observertypes.GetBlameIndex(chainID, nonce, digestHex, height) + zetaHash, err := s.zetacore.PostVoteBlameData(ctx, &res.Blame, chainID, index) + if err != nil { + return errors.Wrap(err, "unable to post blame data for failed keysign") + } + + s.logger.Info(). + Fields(lf). + Str("keygen.blame_tx_hash", zetaHash). + Msg("Posted blame data to zetacore") + + return errFailure +} + +// combineDigests combines the digests +func combineDigests(digestList []string) []byte { + digestConcat := strings.Join(digestList, "") + digestBytes := chainhash.DoubleHashH([]byte(digestConcat)) + return digestBytes.CloneBytes() +} + +func keysignLogFields(req keysign.Request, height, nonce uint64, chainID int64) map[string]any { + return map[string]any{ + "keysign.chain_id": chainID, + "keysign.block_height": height, + "keysign.nonce": nonce, + "keysign.request": req, + } +} diff --git a/zetaclient/tss/service_test.go b/zetaclient/tss/service_test.go new file mode 100644 index 0000000000..eeb126c03f --- /dev/null +++ b/zetaclient/tss/service_test.go @@ -0,0 +1,200 @@ +package tss + +import ( + "context" + "crypto/ecdsa" + "crypto/rand" + "encoding/base64" + "fmt" + "regexp" + "testing" + + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + "github.com/ethereum/go-ethereum/crypto" + "github.com/rs/zerolog" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/cmd" + "github.com/zeta-chain/node/pkg/cosmos" + "github.com/zeta-chain/node/zetaclient/testutils/mocks" + "gitlab.com/thorchain/tss/go-tss/blame" + tsscommon "gitlab.com/thorchain/tss/go-tss/common" + "gitlab.com/thorchain/tss/go-tss/keysign" +) + +func TestService(t *testing.T) { + cmd.SetupCosmosConfig() + + t.Run("NewService", func(t *testing.T) { + t.Run("Invalid pub key", func(t *testing.T) { + s, err := NewService(nil, "hello", nil, zerolog.Nop()) + require.ErrorContains(t, err, "invalid tss pub key") + require.Empty(t, s) + }) + + t.Run("Creates new service", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t) + + // ACT + s, err := NewService(ts, ts.PubKeyBech32(), ts.zetacore, ts.logger) + + // ASSERT + require.NoError(t, err) + require.NotNil(t, s) + assert.Regexp(t, regexp.MustCompile(`^zetapub.+$`), s.PubKey().Bech32String()) + assert.Equal(t, ts.PubKeyBech32(), s.PubKey().Bech32String()) + }) + }) + + t.Run("Sign", func(t *testing.T) { + t.Run("Success", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t) + + // Given tss service + s, err := NewService(ts, ts.PubKeyBech32(), ts.zetacore, ts.logger) + require.NoError(t, err) + + // Given a sample msg to sign + digest := ts.SampleDigest() + + // Given mock response + const blockHeight = 123 + ts.keySignerMock.AddCall(ts.PubKeyBech32(), [][]byte{digest}, blockHeight, true, nil) + + // ACT + // Sign a message + // - note that Sign() also contains sig verification + // - note that Sign() is a wrapper for SignBatch() + sig, err := s.Sign(ts.ctx, digest, blockHeight, 2, 3) + + // ASSERT + require.NoError(t, err) + require.NotEmpty(t, sig) + }) + }) +} + +type testSuite struct { + *keySignerMock + ctx context.Context + zetacore *mocks.ZetacoreClient + logger zerolog.Logger +} + +func newTestSuite(t *testing.T) *testSuite { + return &testSuite{ + keySignerMock: newKeySignerMock(t), + ctx: context.Background(), + zetacore: mocks.NewZetacoreClient(t), + logger: zerolog.New(zerolog.NewTestWriter(t)), + } +} + +func (ts *testSuite) SampleDigest() []byte { + var digest [32]byte + + _, err := rand.Reader.Read(digest[:]) + require.NoError(ts.t, err) + + return digest[:] +} + +type keySignerMock struct { + t *testing.T + privateKey *ecdsa.PrivateKey + mocks map[string]lo.Tuple2[keysign.Response, error] +} + +func newKeySignerMock(t *testing.T) *keySignerMock { + privateKey, err := crypto.GenerateKey() + require.NoError(t, err) + + return &keySignerMock{ + t: t, + privateKey: privateKey, + mocks: map[string]lo.Tuple2[keysign.Response, error]{}, + } +} + +func (m *keySignerMock) PubKeyBech32() string { + cosmosPrivateKey := &secp256k1.PrivKey{Key: m.privateKey.D.Bytes()} + pk := cosmosPrivateKey.PubKey() + + bech32, err := cosmos.Bech32ifyPubKey(cosmos.Bech32PubKeyTypeAccPub, pk) + require.NoError(m.t, err) + + return bech32 +} + +func (m *keySignerMock) AddCall(pk string, digests [][]byte, height int64, success bool, err error) { + if success && err != nil { + m.t.Fatalf("success and error are mutually exclusive") + } + + var ( + msgs = lo.Map(digests, func(digest []byte, _ int) string { + return base64.StdEncoding.EncodeToString(digest) + }) + + req = keysign.NewRequest(pk, msgs, height, nil, Version) + key = m.key(req) + + res keysign.Response + ) + + if !success { + res = keysign.Response{ + Status: tsscommon.Fail, + Blame: blame.Blame{ + FailReason: "Ooopsie", + BlameNodes: []blame.Node{{Pubkey: "some pub key"}}, + }, + } + m.mocks[key] = lo.Tuple2[keysign.Response, error]{A: res} + return + } + + res = m.sign(req) + m.mocks[key] = lo.Tuple2[keysign.Response, error]{A: res} +} + +// sign actually signs the message using local private key instead of TSS +func (m *keySignerMock) sign(req keysign.Request) keysign.Response { + var signatures []keysign.Signature + + for _, msg := range req.Messages { + digest, err := base64DecodeString(msg) + require.NoError(m.t, err) + + // [R || S || V] + sig, err := crypto.Sign(digest, m.privateKey) + require.NoError(m.t, err) + + signatures = append(signatures, keysign.Signature{ + Msg: msg, + R: base64.StdEncoding.EncodeToString(sig[:32]), + S: base64.StdEncoding.EncodeToString(sig[32:64]), + RecoveryID: base64.StdEncoding.EncodeToString(sig[64:65]), + }) + } + + return keysign.Response{ + Signatures: signatures, + Status: tsscommon.Success, + } +} + +func (m *keySignerMock) KeySign(req keysign.Request) (keysign.Response, error) { + key := m.key(req) + v, ok := m.mocks[key] + require.True(m.t, ok, "unexpected call KeySign(%+v)", req) + + return v.Unpack() +} + +func (m *keySignerMock) key(req keysign.Request) string { + return fmt.Sprintf("%s-%d:[%+v]", req.PoolPubKey, req.BlockHeight, req.Messages) +} From 82778ed8c9bd6c35db02b8cf6653a6ee68aaeffe Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Tue, 19 Nov 2024 14:38:32 +0100 Subject: [PATCH 07/37] Add prometheus metrics --- zetaclient/tss/keysign.go | 5 +- zetaclient/tss/service.go | 96 ++++++++++++++++++++++++++++++++------- 2 files changed, 81 insertions(+), 20 deletions(-) diff --git a/zetaclient/tss/keysign.go b/zetaclient/tss/keysign.go index b10918a96f..6164241979 100644 --- a/zetaclient/tss/keysign.go +++ b/zetaclient/tss/keysign.go @@ -9,7 +9,6 @@ import ( "github.com/rs/zerolog" tsscommon "gitlab.com/thorchain/tss/go-tss/common" "gitlab.com/thorchain/tss/go-tss/keysign" - "gitlab.com/thorchain/tss/go-tss/tss" "github.com/zeta-chain/node/pkg/cosmos" "github.com/zeta-chain/node/zetaclient/logs" @@ -22,7 +21,7 @@ var ( ) // TestKeySign performs a TSS key-sign test of sample data. -func TestKeySign(tssServer *tss.TssServer, tssPubKey string, logger zerolog.Logger) error { +func TestKeySign(keySigner KeySigner, tssPubKey string, logger zerolog.Logger) error { logger = logger.With().Str(logs.FieldModule, "tss_keysign").Logger() hashedData := crypto.Keccak256Hash(testKeySignData) @@ -40,7 +39,7 @@ func TestKeySign(tssServer *tss.TssServer, tssPubKey string, logger zerolog.Logg Version, ) - res, err := tssServer.KeySign(req) + res, err := keySigner.KeySign(req) switch { case err != nil: return errors.Wrap(err, "key signing request error") diff --git a/zetaclient/tss/service.go b/zetaclient/tss/service.go index b9b69f1718..0457462a6d 100644 --- a/zetaclient/tss/service.go +++ b/zetaclient/tss/service.go @@ -6,9 +6,11 @@ import ( "encoding/hex" "fmt" "strings" + "time" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" "github.com/rs/zerolog" thorcommon "gitlab.com/thorchain/tss/go-tss/common" "gitlab.com/thorchain/tss/go-tss/keysign" @@ -30,11 +32,21 @@ type Service struct { currentPubKey PubKey postBlame bool - logger zerolog.Logger + metrics *Metrics + + logger zerolog.Logger +} + +// Metrics Prometheus metrics for the TSS service. +type Metrics struct { + ActiveMsgsSigns prometheus.Gauge + SignLatency *prometheus.HistogramVec + NodeBlamePerPubKey *prometheus.CounterVec } type serviceConfig struct { postBlame bool + metrics *Metrics } // Opt Service option. @@ -48,11 +60,36 @@ func WithPostBlame(postBlame bool) Opt { } } +// WithMetrics registers Prometheus metrics for the TSS service. +// Otherwise, no metrics will be collected. +func WithMetrics(ctx context.Context, zetacore interfaces.ZetacoreClient, m *Metrics) Opt { + return func(cfg *serviceConfig, _ zerolog.Logger) error { + keygen, err := zetacore.GetKeyGen(ctx) + if err != nil { + return errors.Wrap(err, "failed to get keygen (WithMetrics)") + } + + m.ActiveMsgsSigns.Set(0) + m.SignLatency.Reset() + m.NodeBlamePerPubKey.Reset() + + for _, granteeBech32 := range keygen.GranteePubkeys { + m.NodeBlamePerPubKey.WithLabelValues(granteeBech32).Inc() + } + + cfg.metrics = m + + return nil + } +} + +var noopMetrics = Metrics{ + ActiveMsgsSigns: prometheus.NewGauge(prometheus.GaugeOpts{Name: "noop"}), + SignLatency: prometheus.NewHistogramVec(prometheus.HistogramOpts{Name: "noop"}, []string{"result"}), + NodeBlamePerPubKey: prometheus.NewCounterVec(prometheus.CounterOpts{Name: "noop"}, []string{"pubkey"}), +} + // NewService Service constructor. -// TODO Constructor -// TODO PubKey struct -// TODO Test cases for bootstrap -// TODO metrics // TODO LRU cache func NewService( keySigner KeySigner, @@ -63,26 +100,30 @@ func NewService( ) (*Service, error) { logger = logger.With().Str(logs.FieldModule, "tss_service").Logger() - // Apply opts - var cfg serviceConfig + cfg := serviceConfig{ + metrics: &noopMetrics, + postBlame: false, + } + for _, opt := range opts { if err := opt(&cfg, logger); err != nil { return nil, errors.Wrap(err, "failed to apply tss config option") } } - currentTSSPubKey, err := NewPubKeyFromBech32(tssPubKeyBech32) + // Represents the current TSS public key. + // FWIW, based on this, we can derive EVM / BTC addresses. + currentPubKey, err := NewPubKeyFromBech32(tssPubKeyBech32) if err != nil { return nil, errors.Wrap(err, "invalid tss pub key") } - // todo metrics - return &Service{ tss: keySigner, - currentPubKey: currentTSSPubKey, + currentPubKey: currentPubKey, zetacore: zc, postBlame: cfg.postBlame, + metrics: cfg.metrics, logger: logger, }, nil } @@ -159,12 +200,30 @@ func (s *Service) SignBatch( return signatures, nil } -func (s *Service) sign(req keysign.Request) (keysign.Response, error) { - // todo track signs (metrics) - res, err := s.tss.KeySign(req) - // todo finish tracking +var ( + signLabelsSuccess = prometheus.Labels{"result": "success"} + signLabelsError = prometheus.Labels{"result": "error"} +) + +// sign sends TSS key sign request to the underlying go-tss and registers metrics +func (s *Service) sign(req keysign.Request) (res keysign.Response, err error) { + // metrics start + messagesCount, start := float64(len(req.Messages)), time.Now() + s.metrics.ActiveMsgsSigns.Add(messagesCount) + + // metrics finish + defer func() { + s.metrics.ActiveMsgsSigns.Sub(messagesCount) + + latency := time.Since(start).Seconds() + if err == nil && res.Status == thorcommon.Success { + s.metrics.SignLatency.With(signLabelsSuccess).Observe(latency) + } else { + s.metrics.SignLatency.With(signLabelsError).Observe(latency) + } + }() - return res, err + return s.tss.KeySign(req) } func (s *Service) blameFailure( @@ -184,7 +243,10 @@ func (s *Service) blameFailure( Interface("keysign.fail_blame", res.Blame). Msg("Keysign failed") - // todo inc blame metrics + // register blame metrics + for _, node := range res.Blame.BlameNodes { + s.metrics.NodeBlamePerPubKey.WithLabelValues(node.Pubkey).Inc() + } if !s.postBlame { return errFailure From ae2479bb5d9d8114e587894769f7c4ec7d5f0dd7 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Tue, 19 Nov 2024 14:48:03 +0100 Subject: [PATCH 08/37] Restructure files --- zetaclient/tss/{keysign.go => crypto.go} | 58 ++++-------------------- zetaclient/tss/keygen.go | 49 ++++++++++++++++++++ zetaclient/tss/service.go | 12 +---- zetaclient/tss/service_test.go | 10 ++-- 4 files changed, 65 insertions(+), 64 deletions(-) rename zetaclient/tss/{keysign.go => crypto.go} (59%) diff --git a/zetaclient/tss/keysign.go b/zetaclient/tss/crypto.go similarity index 59% rename from zetaclient/tss/keysign.go rename to zetaclient/tss/crypto.go index 6164241979..1198e9dd4b 100644 --- a/zetaclient/tss/keysign.go +++ b/zetaclient/tss/crypto.go @@ -3,67 +3,22 @@ package tss import ( "bytes" "encoding/base64" + "strings" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/ethereum/go-ethereum/crypto" "github.com/pkg/errors" - "github.com/rs/zerolog" - tsscommon "gitlab.com/thorchain/tss/go-tss/common" "gitlab.com/thorchain/tss/go-tss/keysign" "github.com/zeta-chain/node/pkg/cosmos" - "github.com/zeta-chain/node/zetaclient/logs" ) var ( - testKeySignData = []byte("hello meta") base64Decode = base64.StdEncoding.Decode base64DecodeString = base64.StdEncoding.DecodeString + base64EncodeString = base64.StdEncoding.EncodeToString ) -// TestKeySign performs a TSS key-sign test of sample data. -func TestKeySign(keySigner KeySigner, tssPubKey string, logger zerolog.Logger) error { - logger = logger.With().Str(logs.FieldModule, "tss_keysign").Logger() - - hashedData := crypto.Keccak256Hash(testKeySignData) - - logger.Info(). - Str("keysign.test_data", string(testKeySignData)). - Str("keysign.test_data_hash", hashedData.String()). - Msg("Performing TSS key-sign test") - - req := keysign.NewRequest( - tssPubKey, - []string{base64.StdEncoding.EncodeToString(hashedData.Bytes())}, - 10, - nil, - Version, - ) - - res, err := keySigner.KeySign(req) - switch { - case err != nil: - return errors.Wrap(err, "key signing request error") - case res.Status != tsscommon.Success: - logger.Error().Interface("keysign.fail_blame", res.Blame).Msg("Keysign failed") - return errors.Wrapf(err, "key signing is not successful (status %d)", res.Status) - case len(res.Signatures) == 0: - return errors.New("signatures list is empty") - } - - // 32B msg hash, 32B R, 32B S, 1B RC - signature := res.Signatures[0] - - logger.Info().Interface("keysign.signature", signature).Msg("Received signature from TSS") - - if _, err = VerifySignature(signature, tssPubKey, hashedData.Bytes()); err != nil { - return errors.Wrap(err, "signature verification failed") - } - - logger.Info().Msg("TSS key-sign test passed") - - return nil -} - // VerifySignature checks that keysign.Signature is valid and origins from expected TSS public key. // Also returns signature as [65]byte (R, S, V) func VerifySignature(sig keysign.Signature, tssPubKey string, expectedMsgHash []byte) ([65]byte, error) { @@ -116,3 +71,10 @@ func SignatureToBytes(input keysign.Signature) (sig [65]byte, err error) { return sig, nil } + +// combineDigests combines the digests +func combineDigests(digestList []string) []byte { + digestConcat := strings.Join(digestList, "") + digestBytes := chainhash.DoubleHashH([]byte(digestConcat)) + return digestBytes.CloneBytes() +} diff --git a/zetaclient/tss/keygen.go b/zetaclient/tss/keygen.go index a70c7bb44a..1594d744d3 100644 --- a/zetaclient/tss/keygen.go +++ b/zetaclient/tss/keygen.go @@ -2,15 +2,18 @@ package tss import ( "context" + "encoding/base64" "encoding/hex" "encoding/json" "fmt" "time" + "github.com/ethereum/go-ethereum/crypto" "github.com/pkg/errors" "github.com/rs/zerolog" tsscommon "gitlab.com/thorchain/tss/go-tss/common" "gitlab.com/thorchain/tss/go-tss/keygen" + "gitlab.com/thorchain/tss/go-tss/keysign" "gitlab.com/thorchain/tss/go-tss/tss" "golang.org/x/crypto/sha3" @@ -229,3 +232,49 @@ func digestReq(req keygen.Request) (string, error) { return digest, nil } + +var testKeySignData = []byte("hello meta") + +// TestKeySign performs a TSS key-sign test of sample data. +func TestKeySign(keySigner KeySigner, tssPubKey string, logger zerolog.Logger) error { + logger = logger.With().Str(logs.FieldModule, "tss_keysign").Logger() + + hashedData := crypto.Keccak256Hash(testKeySignData) + + logger.Info(). + Str("keysign.test_data", string(testKeySignData)). + Str("keysign.test_data_hash", hashedData.String()). + Msg("Performing TSS key-sign test") + + req := keysign.NewRequest( + tssPubKey, + []string{base64.StdEncoding.EncodeToString(hashedData.Bytes())}, + 10, + nil, + Version, + ) + + res, err := keySigner.KeySign(req) + switch { + case err != nil: + return errors.Wrap(err, "key signing request error") + case res.Status != tsscommon.Success: + logger.Error().Interface("keysign.fail_blame", res.Blame).Msg("Keysign failed") + return errors.Wrapf(err, "key signing is not successful (status %d)", res.Status) + case len(res.Signatures) == 0: + return errors.New("signatures list is empty") + } + + // 32B msg hash, 32B R, 32B S, 1B RC + signature := res.Signatures[0] + + logger.Info().Interface("keysign.signature", signature).Msg("Received signature from TSS") + + if _, err = VerifySignature(signature, tssPubKey, hashedData.Bytes()); err != nil { + return errors.Wrap(err, "signature verification failed") + } + + logger.Info().Msg("TSS key-sign test passed") + + return nil +} diff --git a/zetaclient/tss/service.go b/zetaclient/tss/service.go index 0457462a6d..7fc03d9972 100644 --- a/zetaclient/tss/service.go +++ b/zetaclient/tss/service.go @@ -2,13 +2,10 @@ package tss import ( "context" - "encoding/base64" "encoding/hex" "fmt" - "strings" "time" - "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "github.com/rs/zerolog" @@ -158,7 +155,7 @@ func (s *Service) SignBatch( digestsBase64 := make([]string, len(digests)) for i, digest := range digests { - digestsBase64[i] = base64.StdEncoding.EncodeToString(digest) + digestsBase64[i] = base64EncodeString(digest) } tssPubKeyBech32 := s.PubKey().Bech32String() @@ -274,13 +271,6 @@ func (s *Service) blameFailure( return errFailure } -// combineDigests combines the digests -func combineDigests(digestList []string) []byte { - digestConcat := strings.Join(digestList, "") - digestBytes := chainhash.DoubleHashH([]byte(digestConcat)) - return digestBytes.CloneBytes() -} - func keysignLogFields(req keysign.Request, height, nonce uint64, chainID int64) map[string]any { return map[string]any{ "keysign.chain_id": chainID, diff --git a/zetaclient/tss/service_test.go b/zetaclient/tss/service_test.go index eeb126c03f..2a6bc619a1 100644 --- a/zetaclient/tss/service_test.go +++ b/zetaclient/tss/service_test.go @@ -4,7 +4,6 @@ import ( "context" "crypto/ecdsa" "crypto/rand" - "encoding/base64" "fmt" "regexp" "testing" @@ -129,6 +128,7 @@ func (m *keySignerMock) PubKeyBech32() string { return bech32 } +// AddCall mimics TSS signature process (when called with provided arguments) func (m *keySignerMock) AddCall(pk string, digests [][]byte, height int64, success bool, err error) { if success && err != nil { m.t.Fatalf("success and error are mutually exclusive") @@ -136,7 +136,7 @@ func (m *keySignerMock) AddCall(pk string, digests [][]byte, height int64, succe var ( msgs = lo.Map(digests, func(digest []byte, _ int) string { - return base64.StdEncoding.EncodeToString(digest) + return base64EncodeString(digest) }) req = keysign.NewRequest(pk, msgs, height, nil, Version) @@ -175,9 +175,9 @@ func (m *keySignerMock) sign(req keysign.Request) keysign.Response { signatures = append(signatures, keysign.Signature{ Msg: msg, - R: base64.StdEncoding.EncodeToString(sig[:32]), - S: base64.StdEncoding.EncodeToString(sig[32:64]), - RecoveryID: base64.StdEncoding.EncodeToString(sig[64:65]), + R: base64EncodeString(sig[:32]), + S: base64EncodeString(sig[32:64]), + RecoveryID: base64EncodeString(sig[64:65]), }) } From 62f9c269aba9279548437bb2366a4ecc081ad3fd Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:10:37 +0100 Subject: [PATCH 09/37] Implement TSS Setup Flow --- zetaclient/tss/config.go | 61 +++++++ zetaclient/tss/service.go | 18 +- zetaclient/tss/setup.go | 352 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 429 insertions(+), 2 deletions(-) create mode 100644 zetaclient/tss/setup.go diff --git a/zetaclient/tss/config.go b/zetaclient/tss/config.go index ae965fd18f..3881c96e3a 100644 --- a/zetaclient/tss/config.go +++ b/zetaclient/tss/config.go @@ -4,10 +4,12 @@ import ( "encoding/json" "os" "path/filepath" + "strings" "github.com/bnb-chain/tss-lib/ecdsa/keygen" "github.com/multiformats/go-multiaddr" "github.com/pkg/errors" + "github.com/rs/zerolog" tsscommon "gitlab.com/thorchain/tss/go-tss/common" ) @@ -53,3 +55,62 @@ func ResolvePreParamsFromPath(path string) (*keygen.LocalPreParams, error) { return &pp, nil } + +// ParsePubKeysFromPath extracts public keys from tss directory. +// Example: `tssPath="~/.tss"`. Contents: +// localstate-zetapub1addwnpepq2fdhcmfyv07s86djjca835l4f2n2ta0c7le6vnl508mseca2s9g6slj0gm.json +// Output: `zetapub1addwnpepq2fdhcmfyv07s86djjca835l4f2n2ta0c7le6vnl508mseca2s9g6slj0gm` +func ParsePubKeysFromPath(tssPath string, logger zerolog.Logger) ([]PubKey, error) { + const prefix = "localstate-" + + files, err := os.ReadDir(tssPath) + if err != nil { + return nil, errors.Wrap(err, "unable to read dir") + } + + var shareFiles []os.DirEntry + for _, file := range files { + if !file.IsDir() && strings.HasPrefix(filepath.Base(file.Name()), prefix) { + shareFiles = append(shareFiles, file) + } + } + + if len(shareFiles) == 0 { + logger.Warn().Msg("No TSS key share files found") + return nil, nil + } + + logger.Info().Msgf("Found TSS %d key share files", len(shareFiles)) + + result := []PubKey{} + for _, entry := range shareFiles { + filename := filepath.Base(entry.Name()) + + if !strings.HasPrefix(filename, prefix) { + logger.Warn().Msgf("Skipping file %s as it doesn't have %q prefix", prefix, filename) + continue + } + + if !strings.HasSuffix(filename, ".json") { + logger.Warn().Msgf("Skipping file %s as it's not .json", filename) + continue + } + + bech32 := strings.TrimSuffix(strings.TrimPrefix(filename, prefix), ".json") + + pubKey, err := NewPubKeyFromBech32(bech32) + if err != nil { + logger.Error().Err(err).Msgf("Unable to create PubKey from %q", bech32) + continue + } + + result = append(result, pubKey) + } + + if len(result) == 0 { + logger.Warn().Msg("No valid TSS pub keys were found") + return nil, nil + } + + return result, nil +} diff --git a/zetaclient/tss/service.go b/zetaclient/tss/service.go index 7fc03d9972..ef1edf4011 100644 --- a/zetaclient/tss/service.go +++ b/zetaclient/tss/service.go @@ -169,7 +169,7 @@ func (s *Service) SignBatch( Version, ) - res, err := s.sign(req) + res, err := s.sign(req, nonce, chainID) switch { case err != nil: // unexpected error (not related to failed key sign) @@ -203,11 +203,19 @@ var ( ) // sign sends TSS key sign request to the underlying go-tss and registers metrics -func (s *Service) sign(req keysign.Request) (res keysign.Response, err error) { +func (s *Service) sign(req keysign.Request, nonce uint64, chainID int64) (res keysign.Response, err error) { // metrics start messagesCount, start := float64(len(req.Messages)), time.Now() s.metrics.ActiveMsgsSigns.Add(messagesCount) + lf := map[string]any{ + "tss.chain_id": chainID, + "tss.block_height": req.BlockHeight, + "tss.nonce": nonce, + } + + s.logger.Info().Fields(lf).Msg("TSS keysign request") + // metrics finish defer func() { s.metrics.ActiveMsgsSigns.Sub(messagesCount) @@ -218,6 +226,12 @@ func (s *Service) sign(req keysign.Request) (res keysign.Response, err error) { } else { s.metrics.SignLatency.With(signLabelsError).Observe(latency) } + + s.logger.Info(). + Fields(lf). + Bool("tss.success", res.Status == thorcommon.Success). + Float64("tss.latency", latency). + Msg("TSS keysign response") }() return s.tss.KeySign(req) diff --git a/zetaclient/tss/setup.go b/zetaclient/tss/setup.go new file mode 100644 index 0000000000..79558f40b1 --- /dev/null +++ b/zetaclient/tss/setup.go @@ -0,0 +1,352 @@ +package tss + +import ( + "context" + "fmt" + "os" + "path" + "slices" + "time" + + "github.com/bnb-chain/tss-lib/ecdsa/keygen" + "github.com/cometbft/cometbft/crypto" + "github.com/cometbft/cometbft/crypto/secp256k1" + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" + "github.com/pkg/errors" + "github.com/rs/zerolog" + tsscommon "gitlab.com/thorchain/tss/go-tss/common" + "gitlab.com/thorchain/tss/go-tss/conversion" + "gitlab.com/thorchain/tss/go-tss/tss" + + observertypes "github.com/zeta-chain/node/x/observer/types" + "github.com/zeta-chain/node/zetaclient/config" + "github.com/zeta-chain/node/zetaclient/keys" + "github.com/zeta-chain/node/zetaclient/logs" + "github.com/zeta-chain/node/zetaclient/metrics" + "github.com/zeta-chain/node/zetaclient/zetacore" +) + +// SetupProps represents options for Setup. +type SetupProps struct { + Config config.Config + Zetacore *zetacore.Client + HotKeyPassword string + BitcoinChainIDs []int64 + PostBlame bool +} + +// Setup beefy function that does all the logic for bootstrapping tss-server, tss signer, +// generating TSS key is needed, etc... +func Setup(ctx context.Context, p SetupProps, logger zerolog.Logger) (*Service, error) { + logger = logger.With().Str(logs.FieldModule, "tss_setup").Logger() + + // 0. Resolve Hot Private Key + hotPrivateKey, err := p.Zetacore.GetKeys().GetPrivateKey(p.HotKeyPassword) + switch { + case err != nil: + return nil, errors.Wrap(err, "unable to get hot private key") + case len(hotPrivateKey.Bytes()) != 32: + return nil, fmt.Errorf("hot privateKey: expect 32 bytes, got %d bytes", len(hotPrivateKey.Bytes())) + } + + p.Zetacore.GetKeys().GetKeybase() + + hotPrivateKeyECDSA := secp256k1.PrivKey(hotPrivateKey.Bytes()[:32]) + + // 1. Parse bootstrap peer if provided + var bootstrapPeers []multiaddr.Multiaddr + if p.Config.Peer != "" { + bp, err := MultiAddressFromString(p.Config.Peer) + if err != nil { + return nil, errors.Wrapf(err, "unable to parse bootstrap peers (%s)", p.Config.Peer) + } + bootstrapPeers = bp + } + + if len(bootstrapPeers) == 0 { + logger.Warn().Msg("No bootstrap peers provided") + } else { + logger.Info().Interface("bootstrap_peers", bootstrapPeers).Msgf("Bootstrap peers") + } + + // 2. Resolve pre-params. We want to enforce pre-params file existence + tssPreParams, err := ResolvePreParamsFromPath(p.Config.PreParamsPath) + if err != nil { + return nil, errors.Wrap(err, "unable to resolve TSS pre-params. Use `zetaclient tss gen-pre-params`") + } + + logger.Info().Msg("Pre-params file resolved") + + // 3. Prepare whitelist of peers + tssKeygen, err := p.Zetacore.GetKeyGen(ctx) + if err != nil { + return nil, errors.Wrap(err, "unable to get TSS keygen") + } + + logger.Info().Msg("Fetched TSS keygen info") + + whitelistedPeers := make([]peer.ID, len(tssKeygen.GranteePubkeys)) + for i, pk := range tssKeygen.GranteePubkeys { + whitelistedPeers[i], err = conversion.Bech32PubkeyToPeerID(pk) + if err != nil { + return nil, errors.Wrap(err, pk) + } + } + + logger.Info().Interface("whitelisted_peers", whitelistedPeers).Msg("Resolved whitelist peers") + + // 4. + // err = newTss.LoadTssFilesFromDirectory(app.Config().TssPath) + // if err != nil { + // return nil, err + // } + // + // _, pubkeyInBech32, err := keys.GetKeyringKeybase(app.Config(), hotkeyPassword) + // if err != nil { + // return nil, err + // } + // + // err = newTss.VerifyKeysharesForPubkeys(tssHistoricalList, pubkeyInBech32) + // if err != nil { + // client.GetLogger().Error().Err(err).Msg("VerifyKeysharesForPubkeys fail") + // } + + // todo bump numbers + // 4. Bootstrap go-tss TSS server + tssServer, err := NewTSSServer( + bootstrapPeers, + whitelistedPeers, + tssPreParams, + hotPrivateKeyECDSA, + p.Config, + p.HotKeyPassword, + logger, + ) + if err != nil { + return nil, errors.Wrap(err, "unable to start TSS server") + } + + logger.Info().Msg("TSS server started") + + // 5. Perform key generation (if needed) + if err = KeygenCeremony(ctx, tssServer, p.Zetacore, logger); err != nil { + return nil, errors.Wrap(err, "unable to perform keygen ceremony") + } + + // 6. Get tss & tss history from zetacore + tssInfo, err := p.Zetacore.GetTSS(ctx) + if err != nil { + return nil, errors.Wrap(err, "unable to get TSS from zetacore") + } + + logger.Info().Msg("Got TSS info from zetacore") + + historicalTSSInfo, err := p.Zetacore.GetTSSHistory(ctx) + if err != nil { + return nil, errors.Wrap(err, "unable to get TSS history") + } + + // 7. Verify key shared for public keys + logger.Info().Msg("Got historical TSS info from zetacore. Verifying key shares...") + if err = verifyKeySharesForPubKeys(p, historicalTSSInfo, logger); err != nil { + return nil, errors.Wrap(err, "unable to verify key shares for pub keys") + } + + logger.Info().Msg("Key shared verified") + + // 8. Optionally test key signing + if p.Config.TestTssKeysign { + if err = TestKeySign(tssServer, tssInfo.TssPubkey, logger); err != nil { + return nil, errors.Wrap(err, "unable to test key signing") + } + } + + // 8. Setup TSS zetaclient service (wrapper around go-tss TssServer) + service, err := NewService( + tssServer, + tssInfo.TssPubkey, + p.Zetacore, + logger, + WithPostBlame(p.PostBlame), + WithMetrics(ctx, p.Zetacore, &Metrics{ + ActiveMsgsSigns: metrics.NumActiveMsgSigns, + SignLatency: metrics.SignLatency, + NodeBlamePerPubKey: metrics.TssNodeBlamePerPubKey, + }), + ) + if err != nil { + return nil, errors.Wrap(err, "unable to create TSS service") + } + + logger.Info().Msg("TSS service created") + + if err = validateAddresses(service, p.BitcoinChainIDs, logger); err != nil { + return nil, errors.Wrap(err, "unable to validate tss addresses") + } + + logger.Info().Msg("TSS addresses validated") + + // todo health checks + + return service, nil +} + +// NewTSSServer creates a new tss.TssServer (go-tss) instance for key signing. +// - bootstrapPeers are used to discover other peers +// - whitelistPeers are the only peers that are allowed in p2p key signing. +// - preParams are the TSS pre-params required for key generation +func NewTSSServer( + bootstrapPeers []multiaddr.Multiaddr, + whitelistPeers []peer.ID, + preParams *keygen.LocalPreParams, + privateKey crypto.PrivKey, + cfg config.Config, + tssPassword string, + logger zerolog.Logger, +) (*tss.TssServer, error) { + switch { + case len(whitelistPeers) == 0 && len(bootstrapPeers) == 0: + return nil, errors.New("no bootstrap peers and whitelist peers provided") + case preParams == nil: + return nil, errors.New("pre-params are nil") + case tssPassword == "": + return nil, errors.New("tss password is empty") + case privateKey == nil: + return nil, errors.New("private key is nil") + case cfg.PublicIP == "": + logger.Warn().Msg("public IP is empty") + } + + tssPath, err := resolveTSSPath(cfg.TssPath, logger) + if err != nil { + return nil, errors.Wrap(err, "unable to resolve TSS path") + } + + tssConfig := tsscommon.TssConfig{ + EnableMonitor: true, // enables prometheus metrics + KeyGenTimeout: 300 * time.Second, // must be shorter than constants.JailTimeKeygen + KeySignTimeout: 30 * time.Second, // must be shorter than constants.JailTimeKeysign + PartyTimeout: 30 * time.Second, + PreParamTimeout: 5 * time.Minute, + } + + tssServer, err := tss.NewTss( + bootstrapPeers, + Port, + privateKey, + tssPath, + tssConfig, + preParams, + cfg.PublicIP, + tssPassword, + whitelistPeers, + ) + if err != nil { + return nil, errors.Wrap(err, "unable to create TSS server") + } + + // fyi: actually it does nothing, just logs "starting the tss servers" + if err = tssServer.Start(); err != nil { + return nil, errors.Wrap(err, "unable to start TSS server") + } + + if isEmptyPeerID(tssServer.GetLocalPeerID()) { + return nil, fmt.Errorf("local peer ID is empty, aborting") + } + + logger.Info().Msgf("TSS local peer ID is %s", tssServer.GetLocalPeerID()) + + return tssServer, nil +} + +func resolveTSSPath(tssPath string, logger zerolog.Logger) (string, error) { + // noop + if tssPath != "" { + return tssPath, nil + } + + home, err := os.UserHomeDir() + if err != nil { + return "", errors.Wrap(err, "unable to get user home dir") + } + + // it's weird that it's `.Tss` instead of `.tss`, + // but I kept this for backward compatibility + const defaultTssDir = ".Tss" + + tssPath = path.Join(home, defaultTssDir) + logger.Warn().Msgf("TSS path is empty, falling back to %s", tssPath) + + return tssPath, nil +} + +// not sure regarding this function, but the logic is the same +// as in the original code (for backward compatibility) +func isEmptyPeerID(id string) bool { + return id == "" || id == "0" || id == "000000000000000000000000000000" || id == peer.ID("").String() +} + +// verifyKeySharesForPubKeys ensures that observer&signer has the correct key shares +func verifyKeySharesForPubKeys(p SetupProps, history []observertypes.TSS, logger zerolog.Logger) error { + // Parse bech32 public keys from tssPath (i.e. zetapub*...) + tssPath, err := resolveTSSPath(p.Config.TssPath, logger) + if err != nil { + return errors.Wrap(err, "unable to resolve TSS path") + } + + pubKeys, err := ParsePubKeysFromPath(tssPath, logger) + if err != nil { + return errors.Wrap(err, "unable to parse public keys from path") + } + + pubKeysSet := make(map[string]PubKey, len(pubKeys)) + for _, k := range pubKeys { + pubKeysSet[k.Bech32String()] = k + } + + // Get observer's public key ("grantee pub key") + _, granteePubKeyBech32, err := keys.GetKeyringKeybase(p.Config, p.HotKeyPassword) + if err != nil { + return errors.Wrap(err, "unable to get keyring key base") + } + + wasPartOfTSS := func(grantees []string) bool { + return slices.Contains(grantees, granteePubKeyBech32) + } + + for _, tssEntry := range history { + if !wasPartOfTSS(tssEntry.TssParticipantList) { + continue + } + + if _, ok := pubKeysSet[tssEntry.TssPubkey]; !ok { + return fmt.Errorf("pubkey %q not found in keyshare", tssEntry.TssPubkey) + } + } + + return nil +} + +// validateAddresses ensures that TSS has valid EVM and BTC addresses. +func validateAddresses(service *Service, btcChainIDs []int64, logger zerolog.Logger) error { + evm := service.PubKey().AddressEVM() + if evm == (ethcommon.Address{}) { + return fmt.Errorf("blank tss evm address is empty") + } + + logger.Info().Str("evm", evm.String()).Msg("EVM address") + + // validate TSS BTC address for each btc chain + for _, chainID := range btcChainIDs { + addr, err := service.PubKey().AddressBTC(chainID) + if err != nil { + return fmt.Errorf("unable to derive BTC address for chain %d", chainID) + } + + logger.Info().Int64("chain_id", chainID).Str("addr", addr.String()).Msg("BTC address") + } + + return nil +} From e1c8bfd6abbd4a6f004c18ee774d5109f9c93645 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:24:49 +0100 Subject: [PATCH 10/37] Adapt some test cases; fix typo --- zetaclient/tss/config_test.go | 63 +++++++++ zetaclient/tss/setup.go | 6 +- zetaclient/tss/tss_signer_test.go | 223 ------------------------------ 3 files changed, 64 insertions(+), 228 deletions(-) create mode 100644 zetaclient/tss/config_test.go delete mode 100644 zetaclient/tss/tss_signer_test.go diff --git a/zetaclient/tss/config_test.go b/zetaclient/tss/config_test.go new file mode 100644 index 0000000000..e21ab2b2c3 --- /dev/null +++ b/zetaclient/tss/config_test.go @@ -0,0 +1,63 @@ +package tss + +import ( + "fmt" + "os" + "testing" + + "github.com/cosmos/cosmos-sdk/testutil/testdata" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/cosmos" + "github.com/zeta-chain/node/pkg/crypto" +) + +func Test_ParsePubKeysFromPath(t *testing.T) { + for _, tt := range []struct { + name string + n int + }{ + {name: "2 keyshare files", n: 2}, + {name: "10 keyshare files", n: 10}, + {name: "No keyshare files", n: 0}, + } { + t.Run(tt.name, func(t *testing.T) { + // ARRANGE + logger := zerolog.New(zerolog.NewTestWriter(t)) + + dir, err := os.MkdirTemp("", "test-tss") + require.NoError(t, err) + + generateKeyShareFiles(t, tt.n, dir) + + // ACT + keys, err := ParsePubKeysFromPath(dir, logger) + + // ASSERT + require.NoError(t, err) + require.Equal(t, tt.n, len(keys)) + }) + } +} + +func generateKeyShareFiles(t *testing.T, n int, dir string) { + err := os.Chdir(dir) + require.NoError(t, err) + for i := 0; i < n; i++ { + _, pubKey, _ := testdata.KeyTestPubAddr() + + spk, err := cosmos.Bech32ifyPubKey(cosmos.Bech32PubKeyTypeAccPub, pubKey) + require.NoError(t, err) + + pk, err := crypto.NewPubKey(spk) + require.NoError(t, err) + + b, err := pk.MarshalJSON() + require.NoError(t, err) + + filename := fmt.Sprintf("localstate-%s.json", pk.String()) + + err = os.WriteFile(filename, b, 0644) + require.NoError(t, err) + } +} diff --git a/zetaclient/tss/setup.go b/zetaclient/tss/setup.go index 79558f40b1..196e6312e7 100644 --- a/zetaclient/tss/setup.go +++ b/zetaclient/tss/setup.go @@ -272,11 +272,7 @@ func resolveTSSPath(tssPath string, logger zerolog.Logger) (string, error) { return "", errors.Wrap(err, "unable to get user home dir") } - // it's weird that it's `.Tss` instead of `.tss`, - // but I kept this for backward compatibility - const defaultTssDir = ".Tss" - - tssPath = path.Join(home, defaultTssDir) + tssPath = path.Join(home, ".tss") logger.Warn().Msgf("TSS path is empty, falling back to %s", tssPath) return tssPath, nil diff --git a/zetaclient/tss/tss_signer_test.go b/zetaclient/tss/tss_signer_test.go deleted file mode 100644 index 8db9e64704..0000000000 --- a/zetaclient/tss/tss_signer_test.go +++ /dev/null @@ -1,223 +0,0 @@ -package tss - -import ( - "fmt" - "os" - "testing" - - "github.com/cosmos/cosmos-sdk/testutil/testdata" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/stretchr/testify/require" - - "github.com/zeta-chain/node/cmd" - "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/pkg/constant" - "github.com/zeta-chain/node/pkg/cosmos" - "github.com/zeta-chain/node/pkg/crypto" - "github.com/zeta-chain/node/zetaclient/testutils" -) - -func setupConfig() { - testConfig := sdk.GetConfig() - testConfig.SetBech32PrefixForAccount(cmd.Bech32PrefixAccAddr, cmd.Bech32PrefixAccPub) - testConfig.SetBech32PrefixForValidator(cmd.Bech32PrefixValAddr, cmd.Bech32PrefixValPub) - testConfig.SetBech32PrefixForConsensusNode(cmd.Bech32PrefixConsAddr, cmd.Bech32PrefixConsPub) - testConfig.SetFullFundraiserPath(cmd.ZetaChainHDPath) - sdk.SetCoinDenomRegex(func() string { - return cmd.DenomRegex - }) -} - -func Test_LoadTssFilesFromDirectory(t *testing.T) { - - tt := []struct { - name string - n int - }{ - { - name: "2 keyshare files", - n: 2, - }, - { - name: "10 keyshare files", - n: 10, - }, - { - name: "No keyshare files", - n: 0, - }, - } - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - tempdir, err := os.MkdirTemp("", "test-tss") - require.NoError(t, err) - err = GenerateKeyshareFiles(tc.n, tempdir) - require.NoError(t, err) - tss := TSS{ - logger: zerolog.New(os.Stdout), - Keys: map[string]*Key{}, - CurrentPubkey: "", - } - err = tss.LoadTssFilesFromDirectory(tempdir) - require.NoError(t, err) - require.Equal(t, tc.n, len(tss.Keys)) - }) - } -} - -func GenerateKeyshareFiles(n int, dir string) error { - setupConfig() - err := os.Chdir(dir) - if err != nil { - return err - } - for i := 0; i < n; i++ { - _, pubKey, _ := testdata.KeyTestPubAddr() - spk, err := cosmos.Bech32ifyPubKey(cosmos.Bech32PubKeyTypeAccPub, pubKey) - if err != nil { - return err - } - pk, err := crypto.NewPubKey(spk) - if err != nil { - return err - } - filename := fmt.Sprintf("localstate-%s", pk.String()) - b, err := pk.MarshalJSON() - if err != nil { - return err - } - err = os.WriteFile(filename, b, 0644) - if err != nil { - return err - } - } - return nil -} - -func Test_EVMAddress(t *testing.T) { - setupConfig() - - tests := []struct { - name string - tssPubkey string - expectedEVMAddr string - }{ - { - name: "should return Athens3 TSS EVM address", - tssPubkey: testutils.TSSPubkeyAthens3, - expectedEVMAddr: testutils.TSSAddressEVMAthens3, - }, - { - name: "should return empty TSS EVM address on invalid TSS pubkey", - tssPubkey: "invalidpubkey", - expectedEVMAddr: constant.EVMZeroAddress, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - tss := TSS{ - CurrentPubkey: tc.tssPubkey, - } - evmAddr := tss.EVMAddress() - require.Equal(t, tc.expectedEVMAddr, evmAddr.String()) - }) - } -} - -func Test_BTCAddress(t *testing.T) { - setupConfig() - - tests := []struct { - name string - tssPubkey string - btcChainID int64 - wantAddr string - }{ - { - name: "Athens3 tss pubkey", - tssPubkey: testutils.TSSPubkeyAthens3, - btcChainID: chains.BitcoinTestnet.ChainId, - wantAddr: testutils.TSSAddressBTCAthens3, - }, - { - name: "local network tss pubkey", - tssPubkey: "zetapub1addwnpepqdax2apf4qmqcaxzae7t4m9xz76mungtppsyw5shvznd52ldy6sjjsjfa3z", - btcChainID: chains.BitcoinRegtest.ChainId, - wantAddr: "bcrt1q30ew8md3rd9fx6n4qx0a9tmz0mz44lzjwxppnu", - }, - { - name: "invalid tss pubkey", - tssPubkey: "invalidpubkey", - btcChainID: chains.BitcoinTestnet.ChainId, - wantAddr: "", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - tss := TSS{ - CurrentPubkey: tc.tssPubkey, - } - address, err := tss.BTCAddress(tc.btcChainID) - if tc.wantAddr != "" { - require.NoError(t, err) - require.Equal(t, tc.wantAddr, address.EncodeAddress()) - } else { - require.Nil(t, address) - } - }) - } -} - -func Test_ValidateAddresses(t *testing.T) { - setupConfig() - - tests := []struct { - name string - tssPubkey string - btcChainIDs []int64 - errMsg string - }{ - { - name: "Validation success", - tssPubkey: testutils.TSSPubkeyAthens3, - btcChainIDs: []int64{ - chains.BitcoinTestnet.ChainId, - chains.BitcoinSignetTestnet.ChainId, - }, - errMsg: "", - }, - { - name: "Validation failed on EVM address", - tssPubkey: "invalidpubkey", // to make EVMAddress() failed - btcChainIDs: []int64{}, - errMsg: "blank tss evm address", - }, - { - name: "Validation failed on BTC address", - tssPubkey: testutils.TSSPubkeyAthens3, - btcChainIDs: []int64{ - chains.BitcoinTestnet.ChainId, - 100, // unknown BTC chain ID - }, - errMsg: "cannot derive btc address", - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - tss := TSS{ - logger: log.Logger, - CurrentPubkey: tc.tssPubkey, - } - err := tss.ValidateAddresses(tc.btcChainIDs) - if tc.errMsg != "" { - require.Contains(t, err.Error(), tc.errMsg) - } else { - require.NoError(t, err) - } - }) - } -} From 7f8cdf4a328f8f5dc192bdcd43d8a5883ea9ece1 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:30:58 +0100 Subject: [PATCH 11/37] Merge pubkey.go with crypto.go --- zetaclient/tss/crypto.go | 84 +++++++++++++++++ .../tss/{pubkey_test.go => crypto_test.go} | 0 zetaclient/tss/pubkey.go | 93 ------------------- 3 files changed, 84 insertions(+), 93 deletions(-) rename zetaclient/tss/{pubkey_test.go => crypto_test.go} (100%) delete mode 100644 zetaclient/tss/pubkey.go diff --git a/zetaclient/tss/crypto.go b/zetaclient/tss/crypto.go index 1198e9dd4b..e47bea1027 100644 --- a/zetaclient/tss/crypto.go +++ b/zetaclient/tss/crypto.go @@ -2,23 +2,95 @@ package tss import ( "bytes" + "crypto/ecdsa" + "crypto/elliptic" "encoding/base64" "strings" + "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + eth "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/pkg/errors" "gitlab.com/thorchain/tss/go-tss/keysign" + "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/cosmos" ) +// PubKey represents TSS public key in various formats. +type PubKey struct { + cosmosPubKey cryptotypes.PubKey + ecdsaPubKey *ecdsa.PublicKey +} + var ( base64Decode = base64.StdEncoding.Decode base64DecodeString = base64.StdEncoding.DecodeString base64EncodeString = base64.StdEncoding.EncodeToString ) +// NewPubKeyFromBech32 creates a new PubKey from a bech32 address. +// Example: `zetapub1addwnpepq2fdhcmfyv07s86djjca835l4f2n2ta0c7le6vnl508mseca2s9g6slj0gm` +func NewPubKeyFromBech32(bech32 string) (PubKey, error) { + if bech32 == "" { + return PubKey{}, errors.New("empty bech32 address") + } + + cosmosPubKey, err := cosmos.GetPubKeyFromBech32(cosmos.Bech32PubKeyTypeAccPub, bech32) + if err != nil { + return PubKey{}, errors.Wrap(err, "unable to GetPubKeyFromBech32") + } + + pubKey, err := crypto.DecompressPubkey(cosmosPubKey.Bytes()) + if err != nil { + return PubKey{}, errors.Wrap(err, "unable to DecompressPubkey") + } + + return PubKey{ + cosmosPubKey: cosmosPubKey, + ecdsaPubKey: pubKey, + }, nil +} + +// Bytes marshals pubKey to bytes either as compressed or uncompressed slice. +// +// In ECDSA, a compressed pubKey includes only the X and a parity bit for the Y, +// allowing the full Y to be reconstructed using the elliptic curve equation, +// thus reducing the key size while maintaining the ability to fully recover the pubKey. +func (k PubKey) Bytes(compress bool) []byte { + pk := k.ecdsaPubKey + if compress { + return elliptic.MarshalCompressed(pk.Curve, pk.X, pk.Y) + } + + return crypto.FromECDSAPub(pk) +} + +// Bech32String returns the bech32 string of the public key. +// Example: `zetapub1addwnpepq2fdhcmfyv07s86djjca835l4f2n2ta0c7le6vnl508mseca2s9g6slj0gm` +func (k PubKey) Bech32String() string { + v, err := cosmos.Bech32ifyPubKey(cosmos.Bech32PubKeyTypeAccPub, k.cosmosPubKey) + + // should not happen as we only set k.cosmosPubKey from the constructor + if err != nil { + panic("PubKey.Bech32String: " + err.Error()) + } + + return v +} + +// AddressBTC returns the bitcoin address of the public key. +func (k PubKey) AddressBTC(chainID int64) (*btcutil.AddressWitnessPubKeyHash, error) { + return bitcoinP2WPKH(k.Bytes(true), chainID) +} + +// AddressEVM returns the ethereum address of the public key. +func (k PubKey) AddressEVM() eth.Address { + return crypto.PubkeyToAddress(*k.ecdsaPubKey) +} + // VerifySignature checks that keysign.Signature is valid and origins from expected TSS public key. // Also returns signature as [65]byte (R, S, V) func VerifySignature(sig keysign.Signature, tssPubKey string, expectedMsgHash []byte) ([65]byte, error) { @@ -78,3 +150,15 @@ func combineDigests(digestList []string) []byte { digestBytes := chainhash.DoubleHashH([]byte(digestConcat)) return digestBytes.CloneBytes() } + +// bitcoinP2WPKH returns P2WPKH (pay to witness pub key hash) address from the compressed pub key. +func bitcoinP2WPKH(pkCompressed []byte, chainID int64) (*btcutil.AddressWitnessPubKeyHash, error) { + params, err := chains.BitcoinNetParamsFromChainID(chainID) + if err != nil { + return nil, errors.Wrap(err, "unable to get btc net params") + } + + hash := btcutil.Hash160(pkCompressed) + + return btcutil.NewAddressWitnessPubKeyHash(hash, params) +} diff --git a/zetaclient/tss/pubkey_test.go b/zetaclient/tss/crypto_test.go similarity index 100% rename from zetaclient/tss/pubkey_test.go rename to zetaclient/tss/crypto_test.go diff --git a/zetaclient/tss/pubkey.go b/zetaclient/tss/pubkey.go deleted file mode 100644 index bd7ba60a9e..0000000000 --- a/zetaclient/tss/pubkey.go +++ /dev/null @@ -1,93 +0,0 @@ -package tss - -import ( - "crypto/ecdsa" - "crypto/elliptic" - - "github.com/btcsuite/btcd/btcutil" - cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" - eth "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/pkg/errors" - - "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/pkg/cosmos" -) - -// PubKey represents TSS public key in various formats. -type PubKey struct { - cosmosPubKey cryptotypes.PubKey - ecdsaPubKey *ecdsa.PublicKey -} - -// NewPubKeyFromBech32 creates a new PubKey from a bech32 address. -// Example: `zetapub1addwnpepq2fdhcmfyv07s86djjca835l4f2n2ta0c7le6vnl508mseca2s9g6slj0gm` -func NewPubKeyFromBech32(bech32 string) (PubKey, error) { - if bech32 == "" { - return PubKey{}, errors.New("empty bech32 address") - } - - cosmosPubKey, err := cosmos.GetPubKeyFromBech32(cosmos.Bech32PubKeyTypeAccPub, bech32) - if err != nil { - return PubKey{}, errors.Wrap(err, "unable to GetPubKeyFromBech32") - } - - pubKey, err := crypto.DecompressPubkey(cosmosPubKey.Bytes()) - if err != nil { - return PubKey{}, errors.Wrap(err, "unable to DecompressPubkey") - } - - return PubKey{ - cosmosPubKey: cosmosPubKey, - ecdsaPubKey: pubKey, - }, nil -} - -// Bytes marshals pubKey to bytes either as compressed or uncompressed slice. -// -// In ECDSA, a compressed pubKey includes only the X and a parity bit for the Y, -// allowing the full Y to be reconstructed using the elliptic curve equation, -// thus reducing the key size while maintaining the ability to fully recover the pubKey. -func (k PubKey) Bytes(compress bool) []byte { - pk := k.ecdsaPubKey - if compress { - return elliptic.MarshalCompressed(pk.Curve, pk.X, pk.Y) - } - - return crypto.FromECDSAPub(pk) -} - -// Bech32String returns the bech32 string of the public key. -// Example: `zetapub1addwnpepq2fdhcmfyv07s86djjca835l4f2n2ta0c7le6vnl508mseca2s9g6slj0gm` -func (k PubKey) Bech32String() string { - v, err := cosmos.Bech32ifyPubKey(cosmos.Bech32PubKeyTypeAccPub, k.cosmosPubKey) - - // should not happen as we only set k.cosmosPubKey from the constructor - if err != nil { - panic("PubKey.Bech32String: " + err.Error()) - } - - return v -} - -// AddressBTC returns the bitcoin address of the public key. -func (k PubKey) AddressBTC(chainID int64) (*btcutil.AddressWitnessPubKeyHash, error) { - return bitcoinP2WPKH(k.Bytes(true), chainID) -} - -// AddressEVM returns the ethereum address of the public key. -func (k PubKey) AddressEVM() eth.Address { - return crypto.PubkeyToAddress(*k.ecdsaPubKey) -} - -// bitcoinP2WPKH returns P2WPKH (pay to witness pub key hash) address from the compressed pub key. -func bitcoinP2WPKH(pkCompressed []byte, chainID int64) (*btcutil.AddressWitnessPubKeyHash, error) { - params, err := chains.BitcoinNetParamsFromChainID(chainID) - if err != nil { - return nil, errors.Wrap(err, "unable to get btc net params") - } - - hash := btcutil.Hash160(pkCompressed) - - return btcutil.NewAddressWitnessPubKeyHash(hash, params) -} From 63a731372124cc5c596f3624a5df3a928b17cb37 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:24:21 +0100 Subject: [PATCH 12/37] Add tss key pass alongside with hotkey pass --- zetaclient/tss/setup.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/zetaclient/tss/setup.go b/zetaclient/tss/setup.go index 196e6312e7..ef1126d9f0 100644 --- a/zetaclient/tss/setup.go +++ b/zetaclient/tss/setup.go @@ -33,8 +33,14 @@ type SetupProps struct { Config config.Config Zetacore *zetacore.Client HotKeyPassword string + TSSKeyPassword string BitcoinChainIDs []int64 PostBlame bool + Telemetry Telemetry +} + +type Telemetry interface { + SetP2PID(string) } // Setup beefy function that does all the logic for bootstrapping tss-server, tss signer, @@ -121,13 +127,17 @@ func Setup(ctx context.Context, p SetupProps, logger zerolog.Logger) (*Service, tssPreParams, hotPrivateKeyECDSA, p.Config, - p.HotKeyPassword, + p.TSSKeyPassword, logger, ) if err != nil { return nil, errors.Wrap(err, "unable to start TSS server") } + if p.Telemetry != nil { + p.Telemetry.SetP2PID(tssServer.GetLocalPeerID()) + } + logger.Info().Msg("TSS server started") // 5. Perform key generation (if needed) From ade21aa87128658058501cfd5682f975f3dd193b Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:24:46 +0100 Subject: [PATCH 13/37] Simplify start.go; drop old tss from zetaclient --- cmd/zetaclientd/start.go | 237 +++---- zetaclient/chains/interfaces/interfaces.go | 8 +- zetaclient/tss/concurrent_keysigns_tracker.go | 63 -- .../tss/concurrent_keysigns_tracker_test.go | 26 - zetaclient/tss/tss_signer.go | 593 ------------------ 5 files changed, 72 insertions(+), 855 deletions(-) delete mode 100644 zetaclient/tss/concurrent_keysigns_tracker.go delete mode 100644 zetaclient/tss/concurrent_keysigns_tracker_test.go delete mode 100644 zetaclient/tss/tss_signer.go diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index 9c028744cd..5088ac2bf1 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -2,39 +2,36 @@ package main import ( "context" - "fmt" "os" "os/signal" "path/filepath" + "strconv" "strings" - "sync" "syscall" - "time" - "github.com/cometbft/cometbft/crypto/secp256k1" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/libp2p/go-libp2p/p2p/protocol/ping" "github.com/pkg/errors" "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "gitlab.com/thorchain/tss/go-tss/conversion" "github.com/zeta-chain/node/pkg/authz" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/constant" zetaos "github.com/zeta-chain/node/pkg/os" - "github.com/zeta-chain/node/pkg/ticker" - observerTypes "github.com/zeta-chain/node/x/observer/types" "github.com/zeta-chain/node/zetaclient/chains/base" "github.com/zeta-chain/node/zetaclient/config" zctx "github.com/zeta-chain/node/zetaclient/context" "github.com/zeta-chain/node/zetaclient/maintenance" "github.com/zeta-chain/node/zetaclient/metrics" "github.com/zeta-chain/node/zetaclient/orchestrator" - mc "github.com/zeta-chain/node/zetaclient/tss" + zetatss "github.com/zeta-chain/node/zetaclient/tss" "github.com/zeta-chain/node/zetaclient/zetacore" ) +const ( + // enables posting blame data to core for failed TSS signatures + envFlagPostBlame = "POST_BLAME" +) + // Start starts zetaclientd process todo revamp // https://github.com/zeta-chain/node/issues/3119 // https://github.com/zeta-chain/node/issues/3112 @@ -61,7 +58,6 @@ func Start(_ *cobra.Command, _ []string) error { return errors.Wrap(err, "initLogger failed") } - // Wait until zetacore has started if cfg.Peer != "" { if err := validatePeer(cfg.Peer); err != nil { return errors.Wrap(err, "unable to validate peer") @@ -142,181 +138,70 @@ func Start(_ *cobra.Command, _ []string) error { startLogger.Info().Msgf("Config is updated from zetacore\n %s", cfg.StringMasked()) - // Generate TSS address . The Tss address is generated through Keygen ceremony. The TSS key is used to sign all outbound transactions . - // The hotkeyPk is private key for the Hotkey. The Hotkey is used to sign all inbound transactions - // Each node processes a portion of the key stored in ~/.tss by default . Custom location can be specified in config file during init. - // After generating the key , the address is set on the zetacore - hotkeyPk, err := zetacoreClient.GetKeys().GetPrivateKey(hotkeyPass) - if err != nil { - startLogger.Error().Err(err).Msg("zetacore client GetPrivateKey error") - } - startLogger.Debug().Msgf("hotkeyPk %s", hotkeyPk.String()) - if len(hotkeyPk.Bytes()) != 32 { - errMsg := fmt.Sprintf("key bytes len %d != 32", len(hotkeyPk.Bytes())) - log.Error().Msg(errMsg) - return errors.New(errMsg) - } - priKey := secp256k1.PrivKey(hotkeyPk.Bytes()[:32]) - - tssBootstrapPeers, err := mc.MultiAddressFromString(cfg.Peer) - if err != nil { - // this is okay, we still have whitelisted peers to connect to - startLogger.Warn().Err(err).Msg("TSS bootstrap peers error") - } - - tssPreParams, err := mc.ResolvePreParamsFromPath(cfg.PreParamsPath) - if err != nil { - return errors.Wrap(err, "unable to resolve TSS pre params. Use `zetaclient tss gen-pre-params`") - } - m, err := metrics.NewMetrics() if err != nil { - log.Error().Err(err).Msg("NewMetrics") - return err + return errors.Wrap(err, "unable to create metrics") } m.Start() metrics.Info.WithLabelValues(constant.Version).Set(1) metrics.LastStartTime.SetToCurrentTime() - var tssHistoricalList []observerTypes.TSS - tssHistoricalList, err = zetacoreClient.GetTSSHistory(ctx) - if err != nil { - startLogger.Error().Err(err).Msg("GetTssHistory error") - } - telemetryServer.SetIPAddress(cfg.PublicIP) - keygen := appContext.GetKeygen() - whitelistedPeers := []peer.ID{} - for _, pk := range keygen.GranteePubkeys { - pid, err := conversion.Bech32PubkeyToPeerID(pk) - if err != nil { - return err - } - whitelistedPeers = append(whitelistedPeers, pid) + tssSetupProps := zetatss.SetupProps{ + Config: cfg, + Zetacore: zetacoreClient, + HotKeyPassword: hotkeyPass, + TSSKeyPassword: tssKeyPass, + BitcoinChainIDs: btcChainIDsFromContext(appContext), + PostBlame: isEnvFlagEnabled(envFlagPostBlame), + Telemetry: telemetryServer, } - // Create TSS server - tssServer, err := mc.SetupTSSServer( - tssBootstrapPeers, - priKey, - tssPreParams, - appContext.Config(), - tssKeyPass, - true, - whitelistedPeers, - ) + tss, err := zetatss.Setup(ctx, tssSetupProps, startLogger) if err != nil { - return fmt.Errorf("SetupTSSServer error: %w", err) + return errors.Wrap(err, "unable to setup TSS service") } - // Set P2P ID for telemetry - telemetryServer.SetP2PID(tssServer.GetLocalPeerID()) - // Creating a channel to listen for os signals (or other signals) signalChannel := make(chan os.Signal, 1) signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM) - go func() { - for { - time.Sleep(30 * time.Second) - ps := tssServer.GetKnownPeers() - metrics.NumConnectedPeers.Set(float64(len(ps))) - telemetryServer.SetConnectedPeers(ps) - } - }() - go func() { - host := tssServer.GetP2PHost() - pingRTT := make(map[peer.ID]int64) - for { - var wg sync.WaitGroup - for _, p := range whitelistedPeers { - wg.Add(1) - go func(p peer.ID) { - defer wg.Done() - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - result := <-ping.Ping(ctx, host, p) - if result.Error != nil { - masterLogger.Error().Err(result.Error).Msg("ping error") - pingRTT[p] = -1 // RTT -1 indicate ping error - return - } - pingRTT[p] = result.RTT.Nanoseconds() - }(p) - } - wg.Wait() - telemetryServer.SetPingRTT(pingRTT) - time.Sleep(30 * time.Second) - } - }() - - // Generate a new TSS if keygen is set and add it into the tss server - // If TSS has already been generated, and keygen was successful ; we use the existing TSS - err = mc.KeygenCeremony(ctx, tssServer, zetacoreClient, masterLogger) - if err != nil { - return errors.Wrap(err, "unable to run tss keygen ceremony") - } - - tss, err := mc.New( - ctx, - zetacoreClient, - tssHistoricalList, - hotkeyPass, - tssServer, - ) - if err != nil { - startLogger.Error().Err(err).Msg("NewTSS error") - return err - } - if cfg.TestTssKeysign { - if err = mc.TestKeySign(tss.Server, tss.CurrentPubkey, startLogger); err != nil { - startLogger.Error().Err(err). - Str("tss.public_key", tss.CurrentPubkey). - Msg("TSS key-sign failed") - } - } - - // Wait for TSS keygen to be successful before proceeding, This is a blocking thread only for a new keygen. - // For existing keygen, this should directly proceed to the next step - _ = ticker.Run(ctx, time.Second, func(ctx context.Context, t *ticker.Ticker) error { - keygen, err = zetacoreClient.GetKeyGen(ctx) - switch { - case err != nil: - startLogger.Warn().Err(err).Msg("Waiting for TSS Keygen to be a success, got error") - case keygen.Status != observerTypes.KeygenStatus_KeyGenSuccess: - startLogger.Warn().Msgf("Waiting for TSS Keygen to be a success, current status %s", keygen.Status) - default: - t.Stop() - } - - return nil - }) - - // Update Current TSS value from zetacore, if TSS keygen is successful, the TSS address is set on zeta-core - // Returns err if the RPC call fails as zeta client needs the current TSS address to be set - // This is only needed in case of a new Keygen , as the TSS address is set on zetacore only after the keygen is successful i.e enough votes have been broadcast - currentTss, err := zetacoreClient.GetTSS(ctx) - if err != nil { - return errors.Wrap(err, "unable to get current TSS") - } - - // Filter supported BTC chain IDs - btcChains := appContext.FilterChains(zctx.Chain.IsBitcoin) - btcChainIDs := make([]int64, len(btcChains)) - for i, chain := range btcChains { - btcChainIDs[i] = chain.ID() - } - - // Make sure the TSS EVM/BTC addresses are well formed. - // Zetaclient should not start if TSS addresses cannot be properly derived. - tss.CurrentPubkey = currentTss.TssPubkey - err = tss.ValidateAddresses(btcChainIDs) - if err != nil { - startLogger.Error().Err(err).Msg("TSS address validation failed") - return err - } + // todo move to tss/healthcheck.go + //go func() { + // for { + // time.Sleep(30 * time.Second) + // ps := tssServer.GetKnownPeers() + // metrics.NumConnectedPeers.Set(float64(len(ps))) + // telemetryServer.SetConnectedPeers(ps) + // } + //}() + //go func() { + // host := tssServer.GetP2PHost() + // pingRTT := make(map[peer.ID]int64) + // for { + // var wg sync.WaitGroup + // for _, p := range whitelistedPeers { + // wg.Add(1) + // go func(p peer.ID) { + // defer wg.Done() + // ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + // defer cancel() + // result := <-ping.Ping(ctx, host, p) + // if result.Error != nil { + // masterLogger.Error().Err(result.Error).Msg("ping error") + // pingRTT[p] = -1 // RTT -1 indicate ping error + // return + // } + // pingRTT[p] = result.RTT.Nanoseconds() + // }(p) + // } + // wg.Wait() + // telemetryServer.SetPingRTT(pingRTT) + // time.Sleep(30 * time.Second) + // } + //}() // Starts various background TSS listeners. // Shuts down zetaclientd if any is triggered. @@ -425,3 +310,21 @@ func isObserverNode(ctx context.Context, client *zetacore.Client) (bool, error) return false, nil } + +func isEnvFlagEnabled(flag string) bool { + v, _ := strconv.ParseBool(os.Getenv(flag)) + return v +} + +func btcChainIDsFromContext(app *zctx.AppContext) []int64 { + var ( + btcChains = app.FilterChains(zctx.Chain.IsBitcoin) + btcChainIDs = make([]int64, len(btcChains)) + ) + + for i, chain := range btcChains { + btcChainIDs[i] = chain.ID() + } + + return btcChainIDs +} diff --git a/zetaclient/chains/interfaces/interfaces.go b/zetaclient/chains/interfaces/interfaces.go index ded4e0f122..c5fac561ff 100644 --- a/zetaclient/chains/interfaces/interfaces.go +++ b/zetaclient/chains/interfaces/interfaces.go @@ -26,6 +26,7 @@ import ( observertypes "github.com/zeta-chain/node/x/observer/types" keyinterfaces "github.com/zeta-chain/node/zetaclient/keys/interfaces" "github.com/zeta-chain/node/zetaclient/outboundprocessor" + "github.com/zeta-chain/node/zetaclient/tss" ) type Order string @@ -226,12 +227,7 @@ type EVMJSONRPCClient interface { // TSSSigner is the interface for TSS signer type TSSSigner interface { - Pubkey() []byte - EVMAddress() ethcommon.Address - EVMAddressList() []ethcommon.Address - BTCAddress(chainID int64) (*btcutil.AddressWitnessPubKeyHash, error) - PubKeyCompressedBytes() []byte - + PubKey() tss.PubKey Sign(ctx context.Context, data []byte, height, nonce uint64, chainID int64) ([65]byte, error) SignBatch(ctx context.Context, digests [][]byte, height, nonce uint64, chainID int64) ([][65]byte, error) } diff --git a/zetaclient/tss/concurrent_keysigns_tracker.go b/zetaclient/tss/concurrent_keysigns_tracker.go deleted file mode 100644 index 7a8f8f4b45..0000000000 --- a/zetaclient/tss/concurrent_keysigns_tracker.go +++ /dev/null @@ -1,63 +0,0 @@ -package tss - -import ( - "sync" - "time" - - "github.com/prometheus/client_golang/prometheus" - "github.com/rs/zerolog" - - "github.com/zeta-chain/node/zetaclient/metrics" -) - -// ConcurrentKeysignsTracker keeps track of concurrent keysigns performed by go-tss -type ConcurrentKeysignsTracker struct { - numActiveMsgSigns int64 - mu sync.Mutex - Logger zerolog.Logger -} - -// NewKeysignsTracker - constructor -func NewKeysignsTracker(logger zerolog.Logger) *ConcurrentKeysignsTracker { - return &ConcurrentKeysignsTracker{ - numActiveMsgSigns: 0, - mu: sync.Mutex{}, - Logger: logger.With().Str("submodule", "ConcurrentKeysignsTracker").Logger(), - } -} - -// StartMsgSign is incrementing the number of active signing ceremonies as well as updating the prometheus metric -// -// Call the returned function to signify the signing is complete -func (k *ConcurrentKeysignsTracker) StartMsgSign() func(bool) { - k.mu.Lock() - defer k.mu.Unlock() - k.numActiveMsgSigns++ - metrics.NumActiveMsgSigns.Inc() - k.Logger.Debug().Msgf("Start TSS message sign, numActiveMsgSigns: %d", k.numActiveMsgSigns) - - startTime := time.Now() - - return func(hasError bool) { - k.mu.Lock() - defer k.mu.Unlock() - if k.numActiveMsgSigns > 0 { - k.numActiveMsgSigns-- - metrics.NumActiveMsgSigns.Dec() - } - k.Logger.Debug().Msgf("End TSS message sign, numActiveMsgSigns: %d", k.numActiveMsgSigns) - - result := "success" - if hasError { - result = "error" - } - metrics.SignLatency.With(prometheus.Labels{"result": result}).Observe(time.Since(startTime).Seconds()) - } -} - -// GetNumActiveMessageSigns gets the current number of active signing ceremonies -func (k *ConcurrentKeysignsTracker) GetNumActiveMessageSigns() int64 { - k.mu.Lock() - defer k.mu.Unlock() - return k.numActiveMsgSigns -} diff --git a/zetaclient/tss/concurrent_keysigns_tracker_test.go b/zetaclient/tss/concurrent_keysigns_tracker_test.go deleted file mode 100644 index 6295095747..0000000000 --- a/zetaclient/tss/concurrent_keysigns_tracker_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package tss - -import ( - "testing" - - "github.com/rs/zerolog" - "github.com/stretchr/testify/require" -) - -func TestKeySignManager_StartMsgSign(t *testing.T) { - ksman := NewKeysignsTracker(zerolog.Logger{}) - ksman.StartMsgSign() - ksman.StartMsgSign() - ksman.StartMsgSign() - ksman.StartMsgSign() - require.Equal(t, int64(4), ksman.GetNumActiveMessageSigns()) -} - -func TestKeySignManager_EndMsgSign(t *testing.T) { - ksman := NewKeysignsTracker(zerolog.Logger{}) - end1 := ksman.StartMsgSign() - end2 := ksman.StartMsgSign() - end1(true) - end2(false) - require.Equal(t, int64(0), ksman.GetNumActiveMessageSigns()) -} diff --git a/zetaclient/tss/tss_signer.go b/zetaclient/tss/tss_signer.go deleted file mode 100644 index 2f0d11aa3a..0000000000 --- a/zetaclient/tss/tss_signer.go +++ /dev/null @@ -1,593 +0,0 @@ -// Package tss provides the TSS signer functionalities for the zetaclient to sign transactions on external chains -// TODO revamp the whole package -// https://github.com/zeta-chain/node/issues/3119 -package tss - -import ( - "bytes" - "context" - "encoding/base64" - "encoding/hex" - "fmt" - "os" - "path" - "path/filepath" - "sort" - "strings" - "time" - - "github.com/bnb-chain/tss-lib/ecdsa/keygen" - "github.com/btcsuite/btcd/btcutil" - "github.com/btcsuite/btcd/chaincfg/chainhash" - tmcrypto "github.com/cometbft/cometbft/crypto" - ethcommon "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - gopeer "github.com/libp2p/go-libp2p/core/peer" - "github.com/multiformats/go-multiaddr" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - thorcommon "gitlab.com/thorchain/tss/go-tss/common" - "gitlab.com/thorchain/tss/go-tss/keysign" - "gitlab.com/thorchain/tss/go-tss/tss" - - "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/pkg/cosmos" - observertypes "github.com/zeta-chain/node/x/observer/types" - "github.com/zeta-chain/node/zetaclient/chains/interfaces" - "github.com/zeta-chain/node/zetaclient/config" - zctx "github.com/zeta-chain/node/zetaclient/context" - "github.com/zeta-chain/node/zetaclient/keys" - "github.com/zeta-chain/node/zetaclient/metrics" -) - -const ( - // envFlagPostBlame is the environment flag to enable posting blame data to core - envFlagPostBlame = "POST_BLAME" -) - -// Key is a struct that holds the public key, bech32 pubkey, and address for the TSS -type Key struct { - PubkeyInBytes []byte - PubkeyInBech32 string - AddressInHex string -} - -// NewTSSKey creates a new TSS key -func NewTSSKey(pk string) (*Key, error) { - TSSKey := &Key{ - PubkeyInBech32: pk, - } - pubkey, err := cosmos.GetPubKeyFromBech32(cosmos.Bech32PubKeyTypeAccPub, pk) - if err != nil { - log.Error().Err(err).Msgf("GetPubKeyFromBech32 from %s", pk) - return nil, fmt.Errorf("GetPubKeyFromBech32: %w", err) - } - - decompresspubkey, err := crypto.DecompressPubkey(pubkey.Bytes()) - if err != nil { - return nil, fmt.Errorf("NewTSS: DecompressPubkey error: %w", err) - } - - TSSKey.PubkeyInBytes = crypto.FromECDSAPub(decompresspubkey) - TSSKey.AddressInHex = crypto.PubkeyToAddress(*decompresspubkey).Hex() - - return TSSKey, nil -} - -var _ interfaces.TSSSigner = (*TSS)(nil) - -// TSS is a struct that holds the server and the keys for TSS -type TSS struct { - Server *tss.TssServer - Keys map[string]*Key // PubkeyInBech32 => TSSKey - CurrentPubkey string - logger zerolog.Logger - Signers []string - ZetacoreClient interfaces.ZetacoreClient - KeysignsTracker *ConcurrentKeysignsTracker -} - -// New TSS constructor -func New( - ctx context.Context, - client interfaces.ZetacoreClient, - tssHistoricalList []observertypes.TSS, - hotkeyPassword string, - tssServer *tss.TssServer, -) (*TSS, error) { - logger := log.With().Str("module", "tss_signer").Logger() - app, err := zctx.FromContext(ctx) - if err != nil { - return nil, err - } - - newTss := TSS{ - Server: tssServer, - Keys: make(map[string]*Key), - CurrentPubkey: app.GetCurrentTssPubKey(), - logger: logger, - ZetacoreClient: client, - KeysignsTracker: NewKeysignsTracker(logger), - } - - err = newTss.LoadTssFilesFromDirectory(app.Config().TssPath) - if err != nil { - return nil, err - } - - _, pubkeyInBech32, err := keys.GetKeyringKeybase(app.Config(), hotkeyPassword) - if err != nil { - return nil, err - } - - err = newTss.VerifyKeysharesForPubkeys(tssHistoricalList, pubkeyInBech32) - if err != nil { - client.GetLogger().Error().Err(err).Msg("VerifyKeysharesForPubkeys fail") - } - - keygenRes, err := newTss.ZetacoreClient.GetKeyGen(ctx) - if err != nil { - return nil, err - } - - // Initialize metrics - for _, key := range keygenRes.GranteePubkeys { - metrics.TssNodeBlamePerPubKey.WithLabelValues(key).Inc() - } - metrics.NumActiveMsgSigns.Set(0) - - newTss.Signers = app.GetKeygen().GranteePubkeys - - return &newTss, nil -} - -// SetupTSSServer creates a new TSS server -// TODO(revamp): move to TSS server file -func SetupTSSServer( - peer []multiaddr.Multiaddr, - privkey tmcrypto.PrivKey, - preParams *keygen.LocalPreParams, - cfg config.Config, - tssPassword string, - enableMonitor bool, - whitelistedPeers []gopeer.ID, -) (*tss.TssServer, error) { - bootstrapPeers := peer - log.Info().Msgf("Peers AddrList %v", bootstrapPeers) - - tsspath := cfg.TssPath - if len(tsspath) == 0 { - log.Error().Msg("empty env TSSPATH") - homedir, err := os.UserHomeDir() - if err != nil { - log.Error().Err(err).Msgf("cannot get UserHomeDir") - return nil, err - } - tsspath = path.Join(homedir, ".Tss") - log.Info().Msgf("create temporary TSSPATH: %s", tsspath) - } - - IP := cfg.PublicIP - if len(IP) == 0 { - log.Info().Msg("empty public IP in config") - } - - tssServer, err := tss.NewTss( - bootstrapPeers, - Port, - privkey, - tsspath, - thorcommon.TssConfig{ - EnableMonitor: enableMonitor, - KeyGenTimeout: 300 * time.Second, // must be shorter than constants.JailTimeKeygen - KeySignTimeout: 30 * time.Second, // must be shorter than constants.JailTimeKeysign - PartyTimeout: 30 * time.Second, - PreParamTimeout: 5 * time.Minute, - }, - preParams, // use pre-generated pre-params if non-nil - IP, // for docker test - tssPassword, - whitelistedPeers, - ) - if err != nil { - log.Error().Err(err).Msg("NewTSS error") - return nil, fmt.Errorf("NewTSS error: %w", err) - } - - err = tssServer.Start() - if err != nil { - log.Error().Err(err).Msg("tss server start error") - } - - log.Info().Msgf("LocalID: %v", tssServer.GetLocalPeerID()) - if tssServer.GetLocalPeerID() == "" || - tssServer.GetLocalPeerID() == "0" || - tssServer.GetLocalPeerID() == "000000000000000000000000000000" || - tssServer.GetLocalPeerID() == gopeer.ID("").String() { - log.Error().Msg("tss server start error") - return nil, fmt.Errorf("tss server start error") - } - - return tssServer, nil -} - -// Pubkey returns the current pubkey -func (tss *TSS) Pubkey() []byte { - return tss.Keys[tss.CurrentPubkey].PubkeyInBytes -} - -// Sign signs a digest -// digest should be Hashes of some data -func (tss *TSS) Sign(ctx context.Context, digest []byte, height, nonce uint64, chainID int64) ([65]byte, error) { - H := digest - log.Debug().Msgf("hash of digest is %s", H) - - tssPubkey := tss.CurrentPubkey - - // #nosec G115 always in range - keysignReq := keysign.NewRequest( - tssPubkey, - []string{base64.StdEncoding.EncodeToString(H)}, - int64(height), - nil, - Version, - ) - end := tss.KeysignsTracker.StartMsgSign() - ksRes, err := tss.Server.KeySign(keysignReq) - end(err != nil || ksRes.Status == thorcommon.Fail) - if err != nil { - log.Warn().Msg("keysign fail") - } - - if ksRes.Status == thorcommon.Fail { - log.Warn().Msgf("keysign status FAIL posting blame to core, blaming node(s): %#v", ksRes.Blame.BlameNodes) - - // post blame data if enabled - if IsEnvFlagEnabled(envFlagPostBlame) { - digest := hex.EncodeToString(digest) - index := observertypes.GetBlameIndex(chainID, nonce, digest, height) - zetaHash, err := tss.ZetacoreClient.PostVoteBlameData(ctx, &ksRes.Blame, chainID, index) - if err != nil { - log.Error().Err(err).Msg("error sending blame data to core") - return [65]byte{}, err - } - log.Info().Msgf("keysign posted blame data tx hash: %s", zetaHash) - } - - // Increment Blame counter - for _, node := range ksRes.Blame.BlameNodes { - metrics.TssNodeBlamePerPubKey.WithLabelValues(node.Pubkey).Inc() - } - } - signature := ksRes.Signatures - - // [{cyP8i/UuCVfQKDsLr1kpg09/CeIHje1FU6GhfmyMD5Q= D4jXTH3/CSgCg+9kLjhhfnNo3ggy9DTQSlloe3bbKAs= eY++Z2LwsuKG1JcghChrsEJ4u9grLloaaFZNtXI3Ujk= AA==}] - // 32B msg hash, 32B R, 32B S, 1B RC - log.Info().Msgf("signature of digest is... %v", signature) - - if len(signature) == 0 { - return [65]byte{}, fmt.Errorf("keysign fail: signature list is empty") - } - - sig, err := VerifySignature(signature[0], tssPubkey, H) - if err != nil { - return [65]byte{}, fmt.Errorf("unable to verify signature: %w", err) - } - - return sig, nil -} - -// SignBatch is hash of some data -// digest should be batch of hashes of some data -func (tss *TSS) SignBatch( - ctx context.Context, - digests [][]byte, - height uint64, - nonce uint64, - chainID int64, -) ([][65]byte, error) { - tssPubkey := tss.CurrentPubkey - digestBase64 := make([]string, len(digests)) - for i, digest := range digests { - digestBase64[i] = base64.StdEncoding.EncodeToString(digest) - } - // #nosec G115 always in range - keysignReq := keysign.NewRequest(tssPubkey, digestBase64, int64(height), nil, "0.14.0") - - end := tss.KeysignsTracker.StartMsgSign() - ksRes, err := tss.Server.KeySign(keysignReq) - end(err != nil || ksRes.Status == thorcommon.Fail) - if err != nil { - log.Warn().Err(err).Msg("keysign fail") - } - - if ksRes.Status == thorcommon.Fail { - log.Warn().Msg("keysign status FAIL posting blame to core") - - // post blame data if enabled - if IsEnvFlagEnabled(envFlagPostBlame) { - digest := combineDigests(digestBase64) - index := observertypes.GetBlameIndex(chainID, nonce, hex.EncodeToString(digest), height) - zetaHash, err := tss.ZetacoreClient.PostVoteBlameData(ctx, &ksRes.Blame, chainID, index) - if err != nil { - log.Error().Err(err).Msg("error sending blame data to core") - return [][65]byte{}, err - } - log.Info().Msgf("keysign posted blame data tx hash: %s", zetaHash) - } - - // Increment Blame counter - for _, node := range ksRes.Blame.BlameNodes { - metrics.TssNodeBlamePerPubKey.WithLabelValues(node.Pubkey).Inc() - } - } - - signatures := ksRes.Signatures - // [{cyP8i/UuCVfQKDsLr1kpg09/CeIHje1FU6GhfmyMD5Q= D4jXTH3/CSgCg+9kLjhhfnNo3ggy9DTQSlloe3bbKAs= eY++Z2LwsuKG1JcghChrsEJ4u9grLloaaFZNtXI3Ujk= AA==}] - // 32B msg hash, 32B R, 32B S, 1B RC - - if len(signatures) != len(digests) { - log.Warn(). - Err(err). - Msgf("signature has length (%d) not equal to length of digests (%d)", len(signatures), len(digests)) - return [][65]byte{}, fmt.Errorf("keysign fail: %s", err) - } - - //if !verifySignatures(tssPubkey, signatures, digests) { - // log.Error().Err(err).Msgf("signature verification failure") - // return [][65]byte{}, fmt.Errorf("signuature verification fail") - //} - pubkey, err := cosmos.GetPubKeyFromBech32(cosmos.Bech32PubKeyTypeAccPub, tssPubkey) - if err != nil { - log.Error().Msg("get pubkey from bech32 fail") - } - sigBytes := make([][65]byte, len(digests)) - for j, H := range digests { - found := false - D := base64.StdEncoding.EncodeToString(H) - for _, signature := range signatures { - if D == signature.Msg { - found = true - _, err = base64.StdEncoding.Decode(sigBytes[j][:32], []byte(signature.R)) - if err != nil { - log.Error().Err(err).Msg("decoding signature R") - return [][65]byte{}, fmt.Errorf("signuature verification fail") - } - _, err = base64.StdEncoding.Decode(sigBytes[j][32:64], []byte(signature.S)) - if err != nil { - log.Error().Err(err).Msg("decoding signature S") - return [][65]byte{}, fmt.Errorf("signuature verification fail") - } - _, err = base64.StdEncoding.Decode(sigBytes[j][64:65], []byte(signature.RecoveryID)) - if err != nil { - log.Error().Err(err).Msg("decoding signature RecoveryID") - return [][65]byte{}, fmt.Errorf("signuature verification fail") - } - sigPublicKey, err := crypto.SigToPub(H, sigBytes[j][:]) - if err != nil { - log.Error().Err(err).Msg("SigToPub error in verify_signature") - return [][65]byte{}, fmt.Errorf("signuature verification fail") - } - compressedPubkey := crypto.CompressPubkey(sigPublicKey) - if !bytes.Equal(pubkey.Bytes(), compressedPubkey) { - log.Warn(). - Msgf("%d-th pubkey %s recovered pubkey %s", j, pubkey.String(), hex.EncodeToString(compressedPubkey)) - return [][65]byte{}, fmt.Errorf("signuature verification fail") - } - } - } - if !found { - log.Error().Err(err).Msg("signature not found") - return [][65]byte{}, fmt.Errorf("signuature verification fail") - } - } - - return sigBytes, nil -} - -// ValidateAddresses try deriving both the EVM and BTC addresses from the pubkey and make sure they are valid. -func (tss *TSS) ValidateAddresses(btcChainIDs []int64) error { - logger := tss.logger.With(). - Str("method", "ValidateAddresses"). - Str("tss.pubkey", tss.CurrentPubkey). - Logger() - - // validate TSS EVM address - evmAddress := tss.EVMAddress() - blankAddress := ethcommon.Address{} - if evmAddress == blankAddress { - return fmt.Errorf("blank tss evm address: %s", evmAddress.String()) - } - logger.Info().Msgf("tss.eth: %s", evmAddress.String()) - - // validate TSS BTC address for each btc chain - for _, chainID := range btcChainIDs { - address, err := tss.BTCAddress(chainID) - if err != nil { - return fmt.Errorf("cannot derive btc address for chain %d from tss pubkey %s", chainID, tss.CurrentPubkey) - } - logger.Info().Msgf("tss.btc [chain %d]: %s", chainID, address.EncodeAddress()) - } - - return nil -} - -// EVMAddress generates an EVM address from pubkey -func (tss *TSS) EVMAddress() ethcommon.Address { - addr, err := GetTssAddrEVM(tss.CurrentPubkey) - if err != nil { - log.Error().Err(err).Msg("getKeyAddr error") - return ethcommon.Address{} - } - return addr -} - -func (tss *TSS) EVMAddressList() []ethcommon.Address { - addresses := make([]ethcommon.Address, 0) - for _, key := range tss.Keys { - addr, err := GetTssAddrEVM(key.PubkeyInBech32) - if err != nil { - log.Error().Err(err).Msg("getKeyAddr error") - return nil - } - addresses = append(addresses, addr) - } - return addresses -} - -// BTCAddress generates a bech32 p2wpkh address from pubkey -func (tss *TSS) BTCAddress(chainID int64) (*btcutil.AddressWitnessPubKeyHash, error) { - addrWPKH, err := getKeyAddrBTCWitnessPubkeyHash(tss.CurrentPubkey, chainID) - if err != nil { - log.Error().Err(err).Msg("BTCAddressPubkeyHash error") - return nil, err - } - return addrWPKH, nil -} - -// PubKeyCompressedBytes returns the compressed bytes of the current pubkey -func (tss *TSS) PubKeyCompressedBytes() []byte { - pubk, err := cosmos.GetPubKeyFromBech32(cosmos.Bech32PubKeyTypeAccPub, tss.CurrentPubkey) - if err != nil { - log.Error().Err(err).Msg("PubKeyCompressedBytes error") - return nil - } - return pubk.Bytes() -} - -// InsertPubKey adds a new key to the TSS keys map -func (tss *TSS) InsertPubKey(pk string) error { - TSSKey, err := NewTSSKey(pk) - if err != nil { - return err - } - tss.Keys[pk] = TSSKey - return nil -} - -// VerifyKeysharesForPubkeys verifies the keyshares present on the node. It checks whether the node has TSS key shares for the TSS ceremonies it was part of. -func (tss *TSS) VerifyKeysharesForPubkeys(tssList []observertypes.TSS, granteePubKey32 string) error { - for _, t := range tssList { - if wasNodePartOfTss(granteePubKey32, t.TssParticipantList) { - if _, ok := tss.Keys[t.TssPubkey]; !ok { - return fmt.Errorf("pubkey %s not found in keyshare", t.TssPubkey) - } - } - } - return nil -} - -// LoadTssFilesFromDirectory loads the TSS files at the directory specified by the `tssPath` -func (tss *TSS) LoadTssFilesFromDirectory(tssPath string) error { - files, err := os.ReadDir(tssPath) - if err != nil { - fmt.Println("ReadDir error :", err.Error()) - return err - } - found := false - - var sharefiles []os.DirEntry - for _, file := range files { - if !file.IsDir() && strings.HasPrefix(filepath.Base(file.Name()), "localstate") { - sharefiles = append(sharefiles, file) - } - } - - if len(sharefiles) > 0 { - sort.SliceStable(sharefiles, func(i, j int) bool { - fi, err := sharefiles[i].Info() - if err != nil { - return false - } - fj, err := sharefiles[j].Info() - if err != nil { - return false - } - return fi.ModTime().After(fj.ModTime()) - }) - tss.logger.Info().Msgf("found %d localstate files", len(sharefiles)) - for _, localStateFile := range sharefiles { - filename := filepath.Base(localStateFile.Name()) - filearray := strings.Split(filename, "-") - if len(filearray) == 2 { - log.Info().Msgf("Found stored Pubkey in local state: %s", filearray[1]) - pk := strings.TrimSuffix(filearray[1], ".json") - - err = tss.InsertPubKey(pk) - if err != nil { - log.Error().Err(err).Msg("InsertPubKey in NewTSS fail") - } - tss.logger.Info().Msgf("registering TSS pubkey %s (eth hex %s)", pk, tss.Keys[pk].AddressInHex) - found = true - } - } - } - - if !found { - log.Info().Msg("TSS Keyshare file NOT found") - } - return nil -} - -// GetTssAddrEVM generates an EVM address from pubkey -func GetTssAddrEVM(tssPubkey string) (ethcommon.Address, error) { - var keyAddr ethcommon.Address - pubk, err := cosmos.GetPubKeyFromBech32(cosmos.Bech32PubKeyTypeAccPub, tssPubkey) - if err != nil { - log.Fatal().Err(err) - return keyAddr, err - } - //keyAddrBytes := pubk.EVMAddress().Bytes() - pubk.Bytes() - decompresspubkey, err := crypto.DecompressPubkey(pubk.Bytes()) - if err != nil { - log.Fatal().Err(err).Msg("decompress err") - return keyAddr, err - } - - keyAddr = crypto.PubkeyToAddress(*decompresspubkey) - - return keyAddr, nil -} - -func IsEnvFlagEnabled(flag string) bool { - value := os.Getenv(flag) - return value == "true" || value == "1" -} - -// combineDigests combines the digests -func combineDigests(digestList []string) []byte { - digestConcat := strings.Join(digestList[:], "") - digestBytes := chainhash.DoubleHashH([]byte(digestConcat)) - return digestBytes.CloneBytes() -} - -// wasNodePartOfTss checks if the node was part of the TSS -// it checks whether a pubkey is part of the list used to generate the TSS , Every TSS generated on the network has its own list of associated public keys -func wasNodePartOfTss(granteePubKey32 string, granteeList []string) bool { - for _, grantee := range granteeList { - if granteePubKey32 == grantee { - return true - } - } - return false -} - -// getKeyAddrBTCWitnessPubkeyHash generates a bech32 p2wpkh address from pubkey -func getKeyAddrBTCWitnessPubkeyHash(tssPubkey string, chainID int64) (*btcutil.AddressWitnessPubKeyHash, error) { - pubk, err := cosmos.GetPubKeyFromBech32(cosmos.Bech32PubKeyTypeAccPub, tssPubkey) - if err != nil { - return nil, err - } - - bitcoinNetParams, err := chains.BitcoinNetParamsFromChainID(chainID) - if err != nil { - return nil, err - } - - addr, err := btcutil.NewAddressWitnessPubKeyHash(btcutil.Hash160(pubk.Bytes()), bitcoinNetParams) - if err != nil { - return nil, err - } - return addr, nil -} From c5b10c6f81b05cfc7b31e0bbb9bf802bf1e163a4 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Wed, 20 Nov 2024 19:01:27 +0100 Subject: [PATCH 14/37] Fix cyclic imports --- cmd/zetaclientd/initconfig.go | 3 +- cmd/zetaclientd/start.go | 49 ++++++++----------- cmd/zetaclientd/utils.go | 32 ++++++------ zetaclient/chains/interfaces/interfaces.go | 6 +++ zetaclient/testutils/mocks/zetacore_client.go | 28 +++++++++++ zetaclient/tss/keygen.go | 5 +- zetaclient/tss/service.go | 36 ++++++++++++-- zetaclient/tss/service_test.go | 17 +++++-- zetaclient/tss/setup.go | 45 ++++------------- 9 files changed, 125 insertions(+), 96 deletions(-) diff --git a/cmd/zetaclientd/initconfig.go b/cmd/zetaclientd/initconfig.go index 6619ce279a..c8a989bacf 100644 --- a/cmd/zetaclientd/initconfig.go +++ b/cmd/zetaclientd/initconfig.go @@ -7,6 +7,7 @@ import ( "github.com/zeta-chain/node/testutil/sample" "github.com/zeta-chain/node/zetaclient/config" + zetatss "github.com/zeta-chain/node/zetaclient/tss" ) // initializeConfigOptions is a set of CLI options for `init` command. @@ -73,7 +74,7 @@ func InitializeConfig(_ *cobra.Command, _ []string) error { // Validate Peer // e.g. /ip4/172.0.2.1/tcp/6668/p2p/16Uiu2HAmACG5DtqmQsHtXg4G2sLS65ttv84e7MrL4kapkjfmhxAp if opts.peer != "" { - if err := validatePeer(opts.peer); err != nil { + if _, err := zetatss.MultiAddressFromString(opts.peer); err != nil { return errors.Wrap(err, "invalid peer address") } } diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index 5088ac2bf1..0fd2b264d8 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -5,7 +5,6 @@ import ( "os" "os/signal" "path/filepath" - "strconv" "strings" "syscall" @@ -20,6 +19,7 @@ import ( "github.com/zeta-chain/node/zetaclient/chains/base" "github.com/zeta-chain/node/zetaclient/config" zctx "github.com/zeta-chain/node/zetaclient/context" + "github.com/zeta-chain/node/zetaclient/keys" "github.com/zeta-chain/node/zetaclient/maintenance" "github.com/zeta-chain/node/zetaclient/metrics" "github.com/zeta-chain/node/zetaclient/orchestrator" @@ -33,7 +33,6 @@ const ( ) // Start starts zetaclientd process todo revamp -// https://github.com/zeta-chain/node/issues/3119 // https://github.com/zeta-chain/node/issues/3112 func Start(_ *cobra.Command, _ []string) error { // Prompt for Hotkey, TSS key-share and relayer key passwords @@ -58,12 +57,6 @@ func Start(_ *cobra.Command, _ []string) error { return errors.Wrap(err, "initLogger failed") } - if cfg.Peer != "" { - if err := validatePeer(cfg.Peer); err != nil { - return errors.Wrap(err, "unable to validate peer") - } - } - masterLogger := logger.Std startLogger := logger.Std.With().Str("module", "startup").Logger() @@ -149,14 +142,20 @@ func Start(_ *cobra.Command, _ []string) error { telemetryServer.SetIPAddress(cfg.PublicIP) + granteePubKeyBech32, err := resolveObserverPubKeyBech32(cfg, hotkeyPass) + if err != nil { + return errors.Wrap(err, "unable to resolve observer pub key bech32") + } + tssSetupProps := zetatss.SetupProps{ - Config: cfg, - Zetacore: zetacoreClient, - HotKeyPassword: hotkeyPass, - TSSKeyPassword: tssKeyPass, - BitcoinChainIDs: btcChainIDsFromContext(appContext), - PostBlame: isEnvFlagEnabled(envFlagPostBlame), - Telemetry: telemetryServer, + Config: cfg, + Zetacore: zetacoreClient, + GranteePubKeyBech32: granteePubKeyBech32, + HotKeyPassword: hotkeyPass, + TSSKeyPassword: tssKeyPass, + BitcoinChainIDs: btcChainIDsFromContext(appContext), + PostBlame: isEnvFlagEnabled(envFlagPostBlame), + Telemetry: telemetryServer, } tss, err := zetatss.Setup(ctx, tssSetupProps, startLogger) @@ -311,20 +310,12 @@ func isObserverNode(ctx context.Context, client *zetacore.Client) (bool, error) return false, nil } -func isEnvFlagEnabled(flag string) bool { - v, _ := strconv.ParseBool(os.Getenv(flag)) - return v -} - -func btcChainIDsFromContext(app *zctx.AppContext) []int64 { - var ( - btcChains = app.FilterChains(zctx.Chain.IsBitcoin) - btcChainIDs = make([]int64, len(btcChains)) - ) - - for i, chain := range btcChains { - btcChainIDs[i] = chain.ID() +func resolveObserverPubKeyBech32(cfg config.Config, hotKeyPassword string) (string, error) { + // Get observer's public key ("grantee pub key") + _, granteePubKeyBech32, err := keys.GetKeyringKeybase(cfg, hotKeyPassword) + if err != nil { + return "", errors.Wrap(err, "unable to get keyring key base") } - return btcChainIDs + return granteePubKeyBech32, nil } diff --git a/cmd/zetaclientd/utils.go b/cmd/zetaclientd/utils.go index 53f2f208bc..f7ef2f91bc 100644 --- a/cmd/zetaclientd/utils.go +++ b/cmd/zetaclientd/utils.go @@ -3,8 +3,8 @@ package main import ( "context" "fmt" - "net" - "strings" + "os" + "strconv" "time" sdk "github.com/cosmos/cosmos-sdk/types" @@ -16,6 +16,7 @@ import ( "github.com/zeta-chain/node/zetaclient/authz" "github.com/zeta-chain/node/zetaclient/chains/interfaces" "github.com/zeta-chain/node/zetaclient/config" + zctx "github.com/zeta-chain/node/zetaclient/context" "github.com/zeta-chain/node/zetaclient/keys" "github.com/zeta-chain/node/zetaclient/zetacore" ) @@ -101,23 +102,20 @@ func waitForZetacoreToCreateBlocks(ctx context.Context, zc interfaces.ZetacoreCl } } -func validatePeer(seedPeer string) error { - parsedPeer := strings.Split(seedPeer, "/") - - if len(parsedPeer) < 7 { - return errors.New("seed peer missing IP or ID or both, seed: " + seedPeer) - } - - seedIP := parsedPeer[2] - seedID := parsedPeer[6] +func isEnvFlagEnabled(flag string) bool { + v, _ := strconv.ParseBool(os.Getenv(flag)) + return v +} - if net.ParseIP(seedIP) == nil { - return errors.New("invalid seed IP address format, seed: " + seedPeer) - } +func btcChainIDsFromContext(app *zctx.AppContext) []int64 { + var ( + btcChains = app.FilterChains(zctx.Chain.IsBitcoin) + btcChainIDs = make([]int64, len(btcChains)) + ) - if len(seedID) == 0 { - return errors.New("seed id is empty, seed: " + seedPeer) + for i, chain := range btcChains { + btcChainIDs[i] = chain.ID() } - return nil + return btcChainIDs } diff --git a/zetaclient/chains/interfaces/interfaces.go b/zetaclient/chains/interfaces/interfaces.go index c5fac561ff..c13a194eb5 100644 --- a/zetaclient/chains/interfaces/interfaces.go +++ b/zetaclient/chains/interfaces/interfaces.go @@ -111,6 +111,12 @@ type ZetacoreClient interface { GetKeyGen(ctx context.Context) (observertypes.Keygen, error) GetTSS(ctx context.Context) (observertypes.TSS, error) GetTSSHistory(ctx context.Context) ([]observertypes.TSS, error) + PostVoteTSS( + ctx context.Context, + tssPubKey string, + keyGenZetaHeight int64, + status chains.ReceiveStatus, + ) (string, error) GetBlockHeight(ctx context.Context) (int64, error) diff --git a/zetaclient/testutils/mocks/zetacore_client.go b/zetaclient/testutils/mocks/zetacore_client.go index 6bbaee022c..dd507ad5b5 100644 --- a/zetaclient/testutils/mocks/zetacore_client.go +++ b/zetaclient/testutils/mocks/zetacore_client.go @@ -863,6 +863,34 @@ func (_m *ZetacoreClient) PostVoteOutbound(ctx context.Context, gasLimit uint64, return r0, r1, r2 } +// PostVoteTSS provides a mock function with given fields: ctx, tssPubKey, keyGenZetaHeight, status +func (_m *ZetacoreClient) PostVoteTSS(ctx context.Context, tssPubKey string, keyGenZetaHeight int64, status chains.ReceiveStatus) (string, error) { + ret := _m.Called(ctx, tssPubKey, keyGenZetaHeight, status) + + if len(ret) == 0 { + panic("no return value specified for PostVoteTSS") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, int64, chains.ReceiveStatus) (string, error)); ok { + return rf(ctx, tssPubKey, keyGenZetaHeight, status) + } + if rf, ok := ret.Get(0).(func(context.Context, string, int64, chains.ReceiveStatus) string); ok { + r0 = rf(ctx, tssPubKey, keyGenZetaHeight, status) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, int64, chains.ReceiveStatus) error); ok { + r1 = rf(ctx, tssPubKey, keyGenZetaHeight, status) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // NewZetacoreClient creates a new instance of ZetacoreClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewZetacoreClient(t interface { diff --git a/zetaclient/tss/keygen.go b/zetaclient/tss/keygen.go index 1594d744d3..19e8a7a233 100644 --- a/zetaclient/tss/keygen.go +++ b/zetaclient/tss/keygen.go @@ -22,7 +22,6 @@ import ( observertypes "github.com/zeta-chain/node/x/observer/types" "github.com/zeta-chain/node/zetaclient/logs" "github.com/zeta-chain/node/zetaclient/metrics" - "github.com/zeta-chain/node/zetaclient/zetacore" ) const ( @@ -32,14 +31,14 @@ const ( type keygenCeremony struct { tss *tss.TssServer - zetacore *zetacore.Client + zetacore Zetacore lastSeenBlock int64 logger zerolog.Logger } // KeygenCeremony runs TSS keygen ceremony as a blocking thread. // Most likely the keygen is already generated, so this function will be a noop. -func KeygenCeremony(ctx context.Context, tssServer *tss.TssServer, zc *zetacore.Client, logger zerolog.Logger) error { +func KeygenCeremony(ctx context.Context, tssServer *tss.TssServer, zc Zetacore, logger zerolog.Logger) error { const interval = time.Second ceremony := keygenCeremony{ diff --git a/zetaclient/tss/service.go b/zetaclient/tss/service.go index ef1edf4011..2990e4b4a8 100644 --- a/zetaclient/tss/service.go +++ b/zetaclient/tss/service.go @@ -9,11 +9,13 @@ import ( "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "github.com/rs/zerolog" + "gitlab.com/thorchain/tss/go-tss/blame" thorcommon "gitlab.com/thorchain/tss/go-tss/common" "gitlab.com/thorchain/tss/go-tss/keysign" + "github.com/zeta-chain/node/pkg/chains" observertypes "github.com/zeta-chain/node/x/observer/types" - "github.com/zeta-chain/node/zetaclient/chains/interfaces" + keyinterfaces "github.com/zeta-chain/node/zetaclient/keys/interfaces" "github.com/zeta-chain/node/zetaclient/logs" ) @@ -22,9 +24,33 @@ type KeySigner interface { KeySign(req keysign.Request) (keysign.Response, error) } +// Zetacore zeta core client. +type Zetacore interface { + GetKeys() keyinterfaces.ObserverKeys + + Chain() chains.Chain + GetBlockHeight(ctx context.Context) (int64, error) + + GetKeyGen(ctx context.Context) (observertypes.Keygen, error) + GetTSS(ctx context.Context) (observertypes.TSS, error) + GetTSSHistory(ctx context.Context) ([]observertypes.TSS, error) + PostVoteTSS( + ctx context.Context, + tssPubKey string, + keyGenZetaHeight int64, + status chains.ReceiveStatus, + ) (string, error) + + PostVoteBlameData(ctx context.Context, blame *blame.Blame, chainID int64, index string) (string, error) +} + +type Telemetry interface { + SetP2PID(id string) +} + // Service TSS service type Service struct { - zetacore interfaces.ZetacoreClient + zetacore Zetacore tss KeySigner currentPubKey PubKey @@ -59,7 +85,7 @@ func WithPostBlame(postBlame bool) Opt { // WithMetrics registers Prometheus metrics for the TSS service. // Otherwise, no metrics will be collected. -func WithMetrics(ctx context.Context, zetacore interfaces.ZetacoreClient, m *Metrics) Opt { +func WithMetrics(ctx context.Context, zetacore Zetacore, m *Metrics) Opt { return func(cfg *serviceConfig, _ zerolog.Logger) error { keygen, err := zetacore.GetKeyGen(ctx) if err != nil { @@ -91,7 +117,7 @@ var noopMetrics = Metrics{ func NewService( keySigner KeySigner, tssPubKeyBech32 string, - zc interfaces.ZetacoreClient, + zetacore Zetacore, logger zerolog.Logger, opts ...Opt, ) (*Service, error) { @@ -118,7 +144,7 @@ func NewService( return &Service{ tss: keySigner, currentPubKey: currentPubKey, - zetacore: zc, + zetacore: zetacore, postBlame: cfg.postBlame, metrics: cfg.metrics, logger: logger, diff --git a/zetaclient/tss/service_test.go b/zetaclient/tss/service_test.go index 2a6bc619a1..e452a343b8 100644 --- a/zetaclient/tss/service_test.go +++ b/zetaclient/tss/service_test.go @@ -1,9 +1,10 @@ -package tss +package tss_test import ( "context" "crypto/ecdsa" "crypto/rand" + "encoding/base64" "fmt" "regexp" "testing" @@ -17,17 +18,23 @@ import ( "github.com/zeta-chain/node/cmd" "github.com/zeta-chain/node/pkg/cosmos" "github.com/zeta-chain/node/zetaclient/testutils/mocks" + "github.com/zeta-chain/node/zetaclient/tss" "gitlab.com/thorchain/tss/go-tss/blame" tsscommon "gitlab.com/thorchain/tss/go-tss/common" "gitlab.com/thorchain/tss/go-tss/keysign" ) +var ( + base64EncodeString = base64.StdEncoding.EncodeToString + base64DecodeString = base64.StdEncoding.DecodeString +) + func TestService(t *testing.T) { cmd.SetupCosmosConfig() t.Run("NewService", func(t *testing.T) { t.Run("Invalid pub key", func(t *testing.T) { - s, err := NewService(nil, "hello", nil, zerolog.Nop()) + s, err := tss.NewService(nil, "hello", nil, zerolog.Nop()) require.ErrorContains(t, err, "invalid tss pub key") require.Empty(t, s) }) @@ -37,7 +44,7 @@ func TestService(t *testing.T) { ts := newTestSuite(t) // ACT - s, err := NewService(ts, ts.PubKeyBech32(), ts.zetacore, ts.logger) + s, err := tss.NewService(ts, ts.PubKeyBech32(), ts.zetacore, ts.logger) // ASSERT require.NoError(t, err) @@ -53,7 +60,7 @@ func TestService(t *testing.T) { ts := newTestSuite(t) // Given tss service - s, err := NewService(ts, ts.PubKeyBech32(), ts.zetacore, ts.logger) + s, err := tss.NewService(ts, ts.PubKeyBech32(), ts.zetacore, ts.logger) require.NoError(t, err) // Given a sample msg to sign @@ -139,7 +146,7 @@ func (m *keySignerMock) AddCall(pk string, digests [][]byte, height int64, succe return base64EncodeString(digest) }) - req = keysign.NewRequest(pk, msgs, height, nil, Version) + req = keysign.NewRequest(pk, msgs, height, nil, tss.Version) key = m.key(req) res keysign.Response diff --git a/zetaclient/tss/setup.go b/zetaclient/tss/setup.go index ef1126d9f0..2a908fbc42 100644 --- a/zetaclient/tss/setup.go +++ b/zetaclient/tss/setup.go @@ -22,25 +22,20 @@ import ( observertypes "github.com/zeta-chain/node/x/observer/types" "github.com/zeta-chain/node/zetaclient/config" - "github.com/zeta-chain/node/zetaclient/keys" "github.com/zeta-chain/node/zetaclient/logs" "github.com/zeta-chain/node/zetaclient/metrics" - "github.com/zeta-chain/node/zetaclient/zetacore" ) // SetupProps represents options for Setup. type SetupProps struct { - Config config.Config - Zetacore *zetacore.Client - HotKeyPassword string - TSSKeyPassword string - BitcoinChainIDs []int64 - PostBlame bool - Telemetry Telemetry -} - -type Telemetry interface { - SetP2PID(string) + Config config.Config + Zetacore Zetacore + GranteePubKeyBech32 string + HotKeyPassword string + TSSKeyPassword string + BitcoinChainIDs []int64 + PostBlame bool + Telemetry Telemetry } // Setup beefy function that does all the logic for bootstrapping tss-server, tss signer, @@ -103,22 +98,6 @@ func Setup(ctx context.Context, p SetupProps, logger zerolog.Logger) (*Service, logger.Info().Interface("whitelisted_peers", whitelistedPeers).Msg("Resolved whitelist peers") - // 4. - // err = newTss.LoadTssFilesFromDirectory(app.Config().TssPath) - // if err != nil { - // return nil, err - // } - // - // _, pubkeyInBech32, err := keys.GetKeyringKeybase(app.Config(), hotkeyPassword) - // if err != nil { - // return nil, err - // } - // - // err = newTss.VerifyKeysharesForPubkeys(tssHistoricalList, pubkeyInBech32) - // if err != nil { - // client.GetLogger().Error().Err(err).Msg("VerifyKeysharesForPubkeys fail") - // } - // todo bump numbers // 4. Bootstrap go-tss TSS server tssServer, err := NewTSSServer( @@ -312,14 +291,8 @@ func verifyKeySharesForPubKeys(p SetupProps, history []observertypes.TSS, logger pubKeysSet[k.Bech32String()] = k } - // Get observer's public key ("grantee pub key") - _, granteePubKeyBech32, err := keys.GetKeyringKeybase(p.Config, p.HotKeyPassword) - if err != nil { - return errors.Wrap(err, "unable to get keyring key base") - } - wasPartOfTSS := func(grantees []string) bool { - return slices.Contains(grantees, granteePubKeyBech32) + return slices.Contains(grantees, p.GranteePubKeyBech32) } for _, tssEntry := range history { From 1657b1a531183c97e27470dd43ca76788615d0e3 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:24:08 +0100 Subject: [PATCH 15/37] Revamp TSS mock. Fix unit tests across zetaclient --- zetaclient/chains/base/observer.go | 4 +- zetaclient/chains/base/observer_test.go | 39 ++-- zetaclient/chains/base/signer_test.go | 6 +- .../chains/bitcoin/observer/observer.go | 2 +- .../chains/bitcoin/observer/observer_test.go | 6 +- .../chains/bitcoin/observer/outbound.go | 2 +- .../chains/bitcoin/observer/outbound_test.go | 16 +- .../chains/bitcoin/observer/rpc_status.go | 2 +- zetaclient/chains/bitcoin/signer/signer.go | 4 +- .../bitcoin/signer/signer_keysign_test.go | 9 +- .../chains/bitcoin/signer/signer_test.go | 17 +- zetaclient/chains/evm/observer/inbound.go | 4 +- .../chains/evm/observer/inbound_test.go | 2 +- .../chains/evm/observer/observer_test.go | 8 +- zetaclient/chains/evm/signer/sign.go | 2 +- zetaclient/chains/evm/signer/sign_test.go | 37 ++- zetaclient/chains/evm/signer/signer.go | 2 +- .../chains/evm/signer/signer_admin_test.go | 39 ++-- zetaclient/chains/evm/signer/signer_test.go | 14 +- .../chains/solana/observer/observer_test.go | 3 +- zetaclient/chains/solana/observer/outbound.go | 5 +- .../chains/solana/observer/outbound_test.go | 11 +- .../chains/ton/observer/inbound_test.go | 2 +- .../chains/ton/observer/observer_test.go | 4 +- zetaclient/chains/ton/observer/outbound.go | 4 +- zetaclient/chains/ton/signer/signer_test.go | 2 +- zetaclient/orchestrator/bootstap_test.go | 4 +- zetaclient/testutils/constant.go | 6 +- zetaclient/testutils/mocks/tss.go | 127 +++++++++++ zetaclient/testutils/mocks/tss_signer.go | 213 ------------------ zetaclient/tss/crypto.go | 31 +++ zetaclient/tss/crypto_test.go | 5 + 32 files changed, 296 insertions(+), 336 deletions(-) create mode 100644 zetaclient/testutils/mocks/tss.go delete mode 100644 zetaclient/testutils/mocks/tss_signer.go diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index 67f38e627a..d8b723d119 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -228,13 +228,13 @@ func (ob *Observer) WithTSS(tss interfaces.TSSSigner) *Observer { func (ob *Observer) TSSAddressString() string { switch ob.chain.Consensus { case chains.Consensus_bitcoin: - address, err := ob.tss.BTCAddress(ob.Chain().ChainId) + address, err := ob.tss.PubKey().AddressBTC(ob.Chain().ChainId) if err != nil { return "" } return address.EncodeAddress() default: - return ob.tss.EVMAddress().String() + return ob.tss.PubKey().AddressEVM().String() } } diff --git a/zetaclient/chains/base/observer_test.go b/zetaclient/chains/base/observer_test.go index 0c53bea35c..f3e90ded06 100644 --- a/zetaclient/chains/base/observer_test.go +++ b/zetaclient/chains/base/observer_test.go @@ -4,15 +4,14 @@ import ( "context" "fmt" "os" + "strings" "testing" "time" - sdk "github.com/cosmos/cosmos-sdk/types" lru "github.com/hashicorp/golang-lru" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/stretchr/testify/require" - "github.com/zeta-chain/node/cmd" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" "github.com/zeta-chain/node/testutil/sample" @@ -23,7 +22,6 @@ import ( zctx "github.com/zeta-chain/node/zetaclient/context" "github.com/zeta-chain/node/zetaclient/db" "github.com/zeta-chain/node/zetaclient/metrics" - "github.com/zeta-chain/node/zetaclient/testutils" "github.com/zeta-chain/node/zetaclient/testutils/mocks" ) @@ -41,7 +39,7 @@ func createObserver(t *testing.T, chain chains.Chain, alertLatency int64) *base. chainParams := *sample.ChainParams(chain.ChainId) chainParams.ConfirmationCount = defaultConfirmationCount zetacoreClient := mocks.NewZetacoreClient(t) - tss := mocks.NewTSSMainnet() + tss := mocks.NewTSS(t) database := createDatabase(t) @@ -70,7 +68,7 @@ func TestNewObserver(t *testing.T) { chainParams := *sample.ChainParams(chain.ChainId) appContext := zctx.New(config.New(false), nil, zerolog.Nop()) zetacoreClient := mocks.NewZetacoreClient(t) - tss := mocks.NewTSSMainnet() + tss := mocks.NewTSS(t) blockCacheSize := base.DefaultBlockCacheSize headersCacheSize := base.DefaultHeaderCacheSize @@ -188,7 +186,7 @@ func TestObserverGetterAndSetter(t *testing.T) { ob := createObserver(t, chain, defaultAlertLatency) // update tss - newTSS := mocks.NewTSSAthens3() + newTSS := mocks.NewTSS(t) ob = ob.WithTSS(newTSS) require.Equal(t, newTSS, ob.TSS()) }) @@ -266,9 +264,6 @@ func TestObserverGetterAndSetter(t *testing.T) { } func TestTSSAddressString(t *testing.T) { - testConfig := sdk.GetConfig() - testConfig.SetBech32PrefixForAccount(cmd.Bech32PrefixAccAddr, cmd.Bech32PrefixAccPub) - tests := []struct { name string chain chains.Chain @@ -278,17 +273,17 @@ func TestTSSAddressString(t *testing.T) { { name: "should return TSS BTC address for Bitcoin chain", chain: chains.BitcoinMainnet, - addrExpected: testutils.TSSAddressBTCMainnet, + addrExpected: "btc", }, { name: "should return TSS EVM address for EVM chain", chain: chains.Ethereum, - addrExpected: testutils.TSSAddressEVMMainnet, + addrExpected: "eth", }, { name: "should return TSS EVM address for other non-BTC chain", chain: chains.SolanaDevnet, - addrExpected: testutils.TSSAddressEVMMainnet, + addrExpected: "eth", }, { name: "should return empty address for unknown BTC chain", @@ -307,14 +302,26 @@ func TestTSSAddressString(t *testing.T) { // force error if needed if tt.forceError { // pause TSS to cause error - tss := mocks.NewTSSMainnet() + tss := mocks.NewTSS(t) tss.Pause() ob = ob.WithTSS(tss) + c := chains.BitcoinRegtest + c.ChainId = 123123123 + ob.WithChain(c) } // get TSS address addr := ob.TSSAddressString() - require.Equal(t, tt.addrExpected, addr) + switch tt.addrExpected { + case "": + require.Equal(t, "", addr) + case "btc": + require.True(t, strings.HasPrefix(addr, "bc")) + case "eth": + require.True(t, strings.HasPrefix(addr, "0x")) + default: + t.Fail() + } }) } } @@ -374,13 +381,13 @@ func TestOutboundID(t *testing.T) { { name: "should get correct outbound id for Ethereum chain", chain: chains.Ethereum, - tss: mocks.NewTSSMainnet(), + tss: mocks.NewTSS(t), nonce: 100, }, { name: "should get correct outbound id for Bitcoin chain", chain: chains.BitcoinMainnet, - tss: mocks.NewTSSMainnet(), + tss: mocks.NewTSS(t), nonce: 200, }, } diff --git a/zetaclient/chains/base/signer_test.go b/zetaclient/chains/base/signer_test.go index 7b3e4b72e2..6a7489741c 100644 --- a/zetaclient/chains/base/signer_test.go +++ b/zetaclient/chains/base/signer_test.go @@ -12,10 +12,10 @@ import ( ) // createSigner creates a new signer for testing -func createSigner(_ *testing.T) *base.Signer { +func createSigner(t *testing.T) *base.Signer { // constructor parameters chain := chains.Ethereum - tss := mocks.NewTSSMainnet() + tss := mocks.NewTSS(t) logger := base.DefaultLogger() // create signer @@ -40,7 +40,7 @@ func TestSignerGetterAndSetter(t *testing.T) { signer := createSigner(t) // update tss - newTSS := mocks.NewTSSAthens3() + newTSS := mocks.NewTSS(t) signer = signer.WithTSS(newTSS) require.Equal(t, newTSS, signer.TSS()) }) diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index f78d81af9c..8a3516f3d0 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -353,7 +353,7 @@ func (ob *Observer) FetchUTXOs(ctx context.Context) error { maxConfirmations := int(bh) // List all unspent UTXOs (160ms) - tssAddr, err := ob.TSS().BTCAddress(ob.Chain().ChainId) + tssAddr, err := ob.TSS().PubKey().AddressBTC(ob.Chain().ChainId) if err != nil { return fmt.Errorf("error getting bitcoin tss address") } diff --git a/zetaclient/chains/bitcoin/observer/observer_test.go b/zetaclient/chains/bitcoin/observer/observer_test.go index 47172f6141..7679b00bc9 100644 --- a/zetaclient/chains/bitcoin/observer/observer_test.go +++ b/zetaclient/chains/bitcoin/observer/observer_test.go @@ -126,7 +126,7 @@ func Test_NewObserver(t *testing.T) { btcClient: btcClient, chainParams: params, coreClient: nil, - tss: mocks.NewTSSMainnet(), + tss: mocks.NewTSS(t), }, { name: "should fail if net params is not found", @@ -134,7 +134,7 @@ func Test_NewObserver(t *testing.T) { btcClient: btcClient, chainParams: params, coreClient: nil, - tss: mocks.NewTSSMainnet(), + tss: mocks.NewTSS(t), errorMessage: "unable to get BTC net params for chain", }, { @@ -143,7 +143,7 @@ func Test_NewObserver(t *testing.T) { btcClient: btcClient, chainParams: params, coreClient: nil, - tss: mocks.NewTSSMainnet(), + tss: mocks.NewTSS(t), before: func() { envVar := base.EnvVarLatestBlockByChain(chain) os.Setenv(envVar, "invalid") diff --git a/zetaclient/chains/bitcoin/observer/outbound.go b/zetaclient/chains/bitcoin/observer/outbound.go index a66559b62e..2e0f3dd9b1 100644 --- a/zetaclient/chains/bitcoin/observer/outbound.go +++ b/zetaclient/chains/bitcoin/observer/outbound.go @@ -560,7 +560,7 @@ func (ob *Observer) checkTSSVin(ctx context.Context, vins []btcjson.Vin, nonce u if nonce > 0 && len(vins) <= 1 { return fmt.Errorf("checkTSSVin: len(vins) <= 1") } - pubKeyTss := hex.EncodeToString(ob.TSS().PubKeyCompressedBytes()) + pubKeyTss := hex.EncodeToString(ob.TSS().PubKey().Bytes(true)) for i, vin := range vins { // The length of the Witness should be always 2 for SegWit inputs. if len(vin.Witness) != 2 { diff --git a/zetaclient/chains/bitcoin/observer/outbound_test.go b/zetaclient/chains/bitcoin/observer/outbound_test.go index 1507c7d416..fd477e64bd 100644 --- a/zetaclient/chains/bitcoin/observer/outbound_test.go +++ b/zetaclient/chains/bitcoin/observer/outbound_test.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcd/btcjson" "github.com/ethereum/go-ethereum/crypto" + "github.com/rs/zerolog" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/zetaclient/db" @@ -25,7 +26,7 @@ func MockBTCObserverMainnet(t *testing.T) *Observer { // setup mock arguments chain := chains.BitcoinMainnet params := mocks.MockChainParams(chain.ChainId, 10) - tss := mocks.NewTSSMainnet() + tss := mocks.NewTSS(t).FakePubKey(testutils.TSSPubKeyMainnet) // create mock rpc client btcClient := mocks.NewBTCRPCClient(t) @@ -34,8 +35,11 @@ func MockBTCObserverMainnet(t *testing.T) *Observer { database, err := db.NewFromSqliteInMemory(true) require.NoError(t, err) + logger := zerolog.New(zerolog.NewTestWriter(t)) + baseLogger := base.Logger{Std: logger, Compliance: logger} + // create Bitcoin observer - ob, err := NewObserver(chain, btcClient, params, nil, tss, 60, database, base.Logger{}, nil) + ob, err := NewObserver(chain, btcClient, params, nil, tss, 60, database, baseLogger, nil) require.NoError(t, err) return ob @@ -46,9 +50,7 @@ func createObserverWithPrivateKey(t *testing.T) *Observer { skHex := "7b8507ba117e069f4a3f456f505276084f8c92aee86ac78ae37b4d1801d35fa8" privateKey, err := crypto.HexToECDSA(skHex) require.NoError(t, err) - tss := &mocks.TSS{ - PrivKey: privateKey, - } + tss := mocks.NewTSSFromPrivateKey(t, privateKey) // create Bitcoin observer with mock tss ob := MockBTCObserverMainnet(t) @@ -61,7 +63,7 @@ func createObserverWithPrivateKey(t *testing.T) *Observer { func createObserverWithUTXOs(t *testing.T) *Observer { // Create Bitcoin observer ob := createObserverWithPrivateKey(t) - tssAddress, err := ob.TSS().BTCAddress(chains.BitcoinTestnet.ChainId) + tssAddress, err := ob.TSS().PubKey().AddressBTC(chains.BitcoinTestnet.ChainId) require.NoError(t, err) // Create 10 dummy UTXOs (22.44 BTC in total) @@ -79,7 +81,7 @@ func mineTxNSetNonceMark(t *testing.T, ob *Observer, nonce uint64, txid string, ob.includedTxResults[outboundID] = &btcjson.GetTransactionResult{TxID: txid} // Set nonce mark - tssAddress, err := ob.TSS().BTCAddress(chains.BitcoinTestnet.ChainId) + tssAddress, err := ob.TSS().PubKey().AddressBTC(chains.BitcoinMainnet.ChainId) require.NoError(t, err) nonceMark := btcjson.ListUnspentResult{ TxID: txid, diff --git a/zetaclient/chains/bitcoin/observer/rpc_status.go b/zetaclient/chains/bitcoin/observer/rpc_status.go index 03688f4aa4..136882b9b9 100644 --- a/zetaclient/chains/bitcoin/observer/rpc_status.go +++ b/zetaclient/chains/bitcoin/observer/rpc_status.go @@ -29,7 +29,7 @@ func (ob *Observer) watchRPCStatus(_ context.Context) error { // checkRPCStatus checks the RPC status of the Bitcoin chain func (ob *Observer) checkRPCStatus() { - tssAddress, err := ob.TSS().BTCAddress(ob.Chain().ChainId) + tssAddress, err := ob.TSS().PubKey().AddressBTC(ob.Chain().ChainId) if err != nil { ob.Logger().Chain.Error().Err(err).Msg("unable to get TSS BTC address") return diff --git a/zetaclient/chains/bitcoin/signer/signer.go b/zetaclient/chains/bitcoin/signer/signer.go index c239627f22..df00c18f81 100644 --- a/zetaclient/chains/bitcoin/signer/signer.go +++ b/zetaclient/chains/bitcoin/signer/signer.go @@ -155,7 +155,7 @@ func (signer *Signer) AddWithdrawTxOutputs( } // 1st output: the nonce-mark btc to TSS self - tssAddrP2WPKH, err := signer.TSS().BTCAddress(signer.Chain().ChainId) + tssAddrP2WPKH, err := signer.TSS().PubKey().AddressBTC(signer.Chain().ChainId) if err != nil { return err } @@ -304,7 +304,7 @@ func (signer *Signer) SignWithdrawTx( S.SetBytes((*[32]byte)(sig65B[32:64])) sig := btcecdsa.NewSignature(R, S) - pkCompressed := signer.TSS().PubKeyCompressedBytes() + pkCompressed := signer.TSS().PubKey().Bytes(true) hashType := txscript.SigHashAll txWitness := wire.TxWitness{append(sig.Serialize(), byte(hashType)), pkCompressed} tx.TxIn[ix].Witness = txWitness diff --git a/zetaclient/chains/bitcoin/signer/signer_keysign_test.go b/zetaclient/chains/bitcoin/signer/signer_keysign_test.go index e21e468649..5a546b487d 100644 --- a/zetaclient/chains/bitcoin/signer/signer_keysign_test.go +++ b/zetaclient/chains/bitcoin/signer/signer_keysign_test.go @@ -35,10 +35,9 @@ func (suite *BTCSignTestSuite) SetupTest() { wif, _ := btcutil.DecodeWIF(pk) privateKey := wif.PrivKey - suite.testSigner = &mocks.TSS{ // fake TSS - PrivKey: privateKey.ToECDSA(), - } - addr, err := suite.testSigner.BTCAddress(chains.BitcoinTestnet.ChainId) + suite.testSigner = mocks.NewTSSFromPrivateKey(suite.T(), privateKey.ToECDSA()) + + addr, err := suite.testSigner.PubKey().AddressBTC(chains.BitcoinTestnet.ChainId) suite.Require().NoError(err) suite.T().Logf("segwit addr: %s", addr) } @@ -159,7 +158,7 @@ func getTSSTX( return "", err } - pkCompressed := tss.PubKeyCompressedBytes() + pkCompressed := tss.PubKey().Bytes(true) txWitness := wire.TxWitness{append(sig.Serialize(), byte(hashType)), pkCompressed} tx.TxIn[0].Witness = txWitness diff --git a/zetaclient/chains/bitcoin/signer/signer_test.go b/zetaclient/chains/bitcoin/signer/signer_test.go index 17fb2dc3de..76926faa2b 100644 --- a/zetaclient/chains/bitcoin/signer/signer_test.go +++ b/zetaclient/chains/bitcoin/signer/signer_test.go @@ -16,6 +16,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/zetaclient/testutils" . "gopkg.in/check.v1" "github.com/zeta-chain/node/pkg/chains" @@ -43,9 +44,9 @@ func (s *BTCSignerSuite) SetUpTest(c *C) { //privkeyBytes := crypto.FromECDSA(privateKey) //c.Logf("privatekey %s", hex.EncodeToString(privkeyBytes)) c.Assert(err, IsNil) - tss := &mocks.TSS{ - PrivKey: privateKey, - } + + tss := mocks.NewTSSFromPrivateKey(c, privateKey) + s.btcSigner, err = NewSigner( chains.Chain{}, tss, @@ -228,8 +229,8 @@ func (s *BTCSignerSuite) TestP2WPH(c *C) { func TestAddWithdrawTxOutputs(t *testing.T) { // Create test signer and receiver address signer, err := NewSigner( - chains.Chain{}, - mocks.NewTSSMainnet(), + chains.BitcoinRegtest, + mocks.NewTSS(t).FakePubKey(testutils.TSSPubKeyMainnet), nil, base.DefaultLogger(), config.BTCConfig{}, @@ -237,7 +238,7 @@ func TestAddWithdrawTxOutputs(t *testing.T) { require.NoError(t, err) // tss address and script - tssAddr, err := signer.TSS().BTCAddress(chains.BitcoinTestnet.ChainId) + tssAddr, err := signer.TSS().PubKey().AddressBTC(chains.BitcoinMainnet.ChainId) require.NoError(t, err) tssScript, err := txscript.PayToAddrScript(tssAddr) require.NoError(t, err) @@ -387,9 +388,7 @@ func TestNewBTCSigner(t *testing.T) { skHex := "7b8507ba117e069f4a3f456f505276084f8c92aee86ac78ae37b4d1801d35fa8" privateKey, err := crypto.HexToECDSA(skHex) require.NoError(t, err) - tss := &mocks.TSS{ - PrivKey: privateKey, - } + tss := mocks.NewTSSFromPrivateKey(t, privateKey) btcSigner, err := NewSigner( chains.Chain{}, tss, diff --git a/zetaclient/chains/evm/observer/inbound.go b/zetaclient/chains/evm/observer/inbound.go index cb409f408e..eb68ac17a4 100644 --- a/zetaclient/chains/evm/observer/inbound.go +++ b/zetaclient/chains/evm/observer/inbound.go @@ -596,7 +596,7 @@ func (ob *Observer) CheckAndVoteInboundTokenGas( } // checks receiver and tx status - if ethcommon.HexToAddress(tx.To) != ob.TSS().EVMAddress() { + if ethcommon.HexToAddress(tx.To) != ob.TSS().PubKey().AddressEVM() { return "", fmt.Errorf("tx.To %s is not TSS address", tx.To) } if receipt.Status != ethtypes.ReceiptStatusSuccessful { @@ -788,7 +788,7 @@ func (ob *Observer) ObserveTSSReceiveInBlock(ctx context.Context, blockNumber ui } for i := range block.Transactions { tx := block.Transactions[i] - if ethcommon.HexToAddress(tx.To) == ob.TSS().EVMAddress() { + if ethcommon.HexToAddress(tx.To) == ob.TSS().PubKey().AddressEVM() { receipt, err := ob.evmClient.TransactionReceipt(ctx, ethcommon.HexToHash(tx.Hash)) if err != nil { return errors.Wrapf(err, "error getting receipt for inbound %s chain %d", tx.Hash, ob.Chain().ChainId) diff --git a/zetaclient/chains/evm/observer/inbound_test.go b/zetaclient/chains/evm/observer/inbound_test.go index e3612678da..1542e5fea1 100644 --- a/zetaclient/chains/evm/observer/inbound_test.go +++ b/zetaclient/chains/evm/observer/inbound_test.go @@ -465,7 +465,7 @@ func Test_ObserveTSSReceiveInBlock(t *testing.T) { block := testutils.LoadEVMBlock(t, TestDataDir, chainID, blockNumber, true) // create mock zetacore client - tss := mocks.NewTSSMainnet() + tss := mocks.NewTSS(t).FakePubKey(testutils.TSSPubKeyMainnet) lastBlock := receipt.BlockNumber.Uint64() + confirmation zetacoreClient := mocks.NewZetacoreClient(t). WithKeys(&keys.Keys{}). diff --git a/zetaclient/chains/evm/observer/observer_test.go b/zetaclient/chains/evm/observer/observer_test.go index 3be38cd86c..523dff1e4a 100644 --- a/zetaclient/chains/evm/observer/observer_test.go +++ b/zetaclient/chains/evm/observer/observer_test.go @@ -117,7 +117,7 @@ func MockEVMObserver( } // use default mock tss if not provided if tss == nil { - tss = mocks.NewTSSMainnet() + tss = mocks.NewTSS(t).FakePubKey(testutils.TSSPubKeyMainnet) } // create AppContext appContext, _ := getAppContext(t, chain, "", ¶ms) @@ -182,7 +182,7 @@ func Test_NewObserver(t *testing.T) { chainParams: params, evmClient: evmClient, evmJSONRPC: mocks.NewMockJSONRPCClient(), - tss: mocks.NewTSSMainnet(), + tss: mocks.NewTSS(t), logger: base.Logger{}, ts: nil, fail: false, @@ -200,7 +200,7 @@ func Test_NewObserver(t *testing.T) { return evmClient }(), evmJSONRPC: mocks.NewMockJSONRPCClient(), - tss: mocks.NewTSSMainnet(), + tss: mocks.NewTSS(t), logger: base.Logger{}, ts: nil, fail: true, @@ -214,7 +214,7 @@ func Test_NewObserver(t *testing.T) { chainParams: params, evmClient: evmClient, evmJSONRPC: mocks.NewMockJSONRPCClient(), - tss: mocks.NewTSSMainnet(), + tss: mocks.NewTSS(t), before: func() { envVar := base.EnvVarLatestBlockByChain(chain) os.Setenv(envVar, "invalid") diff --git a/zetaclient/chains/evm/signer/sign.go b/zetaclient/chains/evm/signer/sign.go index 6e5c188d02..d753e97c07 100644 --- a/zetaclient/chains/evm/signer/sign.go +++ b/zetaclient/chains/evm/signer/sign.go @@ -111,7 +111,7 @@ func (signer *Signer) SignCancel(ctx context.Context, txData *OutboundData) (*et tx, _, _, err := signer.Sign( ctx, nil, - signer.TSS().EVMAddress(), + signer.TSS().PubKey().AddressEVM(), zeroValue, // zero out the amount to cancel the tx txData.gas, txData.nonce, diff --git a/zetaclient/chains/evm/signer/sign_test.go b/zetaclient/chains/evm/signer/sign_test.go index c3d64ebabc..523744f053 100644 --- a/zetaclient/chains/evm/signer/sign_test.go +++ b/zetaclient/chains/evm/signer/sign_test.go @@ -8,7 +8,6 @@ import ( "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/zetaclient/testutils/mocks" ) @@ -16,8 +15,8 @@ func TestSigner_SignConnectorOnReceive(t *testing.T) { ctx := makeCtx(t) // Setup evm signer - tss := mocks.NewDerivedTSS(chains.BitcoinMainnet) - evmSigner, err := getNewEvmSigner(tss) + tss := mocks.NewTSS(t) + evmSigner, err := getNewEvmSigner(t, tss) require.NoError(t, err) // Setup txData struct @@ -34,7 +33,7 @@ func TestSigner_SignConnectorOnReceive(t *testing.T) { require.NoError(t, err) // Verify Signature - verifyTxSender(t, tx, tss.EVMAddress(), evmSigner.EvmSigner()) + verifyTxSender(t, tx, tss.PubKey().AddressEVM(), evmSigner.EvmSigner()) }) t.Run("SignConnectorOnReceive - should fail if keysign fails", func(t *testing.T) { // Pause tss to make keysign fail @@ -53,7 +52,7 @@ func TestSigner_SignConnectorOnReceive(t *testing.T) { require.NoError(t, err) // Verify Signature - verifyTxSender(t, tx, tss.EVMAddress(), evmSigner.EvmSigner()) + verifyTxSender(t, tx, tss.PubKey().AddressEVM(), evmSigner.EvmSigner()) // check that by default tx type is legacy tx assert.Equal(t, ethtypes.LegacyTxType, int(tx.Type())) @@ -85,7 +84,7 @@ func TestSigner_SignConnectorOnReceive(t *testing.T) { require.NoError(t, err) // ASSERT - verifyTxSender(t, tx, tss.EVMAddress(), evmSigner.EvmSigner()) + verifyTxSender(t, tx, tss.PubKey().AddressEVM(), evmSigner.EvmSigner()) // check that by default tx type is a dynamic fee tx assert.Equal(t, ethtypes.DynamicFeeTxType, int(tx.Type())) @@ -100,8 +99,8 @@ func TestSigner_SignConnectorOnRevert(t *testing.T) { ctx := makeCtx(t) // Setup evm signer - tss := mocks.NewDerivedTSS(chains.BitcoinMainnet) - evmSigner, err := getNewEvmSigner(tss) + tss := mocks.NewTSS(t) + evmSigner, err := getNewEvmSigner(t, tss) require.NoError(t, err) // Setup txData struct @@ -117,7 +116,7 @@ func TestSigner_SignConnectorOnRevert(t *testing.T) { require.NoError(t, err) // Verify tx signature - verifyTxSender(t, tx, tss.EVMAddress(), evmSigner.EvmSigner()) + verifyTxSender(t, tx, tss.PubKey().AddressEVM(), evmSigner.EvmSigner()) // Verify tx body basics // Note: Revert tx calls connector contract with 0 gas token @@ -138,8 +137,8 @@ func TestSigner_SignCancel(t *testing.T) { ctx := makeCtx(t) // Setup evm signer - tss := mocks.NewDerivedTSS(chains.BitcoinMainnet) - evmSigner, err := getNewEvmSigner(tss) + tss := mocks.NewTSS(t) + evmSigner, err := getNewEvmSigner(t, tss) require.NoError(t, err) // Setup txData struct @@ -155,11 +154,11 @@ func TestSigner_SignCancel(t *testing.T) { require.NoError(t, err) // Verify tx signature - verifyTxSender(t, tx, tss.EVMAddress(), evmSigner.EvmSigner()) + verifyTxSender(t, tx, tss.PubKey().AddressEVM(), evmSigner.EvmSigner()) // Verify tx body basics // Note: Cancel tx sends 0 gas token to TSS self address - verifyTxBodyBasics(t, tx, tss.EVMAddress(), txData.nonce, big.NewInt(0)) + verifyTxBodyBasics(t, tx, tss.PubKey().AddressEVM(), txData.nonce, big.NewInt(0)) }) t.Run("SignCancel - should fail if keysign fails", func(t *testing.T) { // Pause tss to make keysign fail @@ -176,8 +175,8 @@ func TestSigner_SignGasWithdraw(t *testing.T) { ctx := makeCtx(t) // Setup evm signer - tss := mocks.NewDerivedTSS(chains.BitcoinMainnet) - evmSigner, err := getNewEvmSigner(tss) + tss := mocks.NewTSS(t) + evmSigner, err := getNewEvmSigner(t, tss) require.NoError(t, err) // Setup txData struct @@ -193,7 +192,7 @@ func TestSigner_SignGasWithdraw(t *testing.T) { require.NoError(t, err) // Verify tx signature - verifyTxSender(t, tx, tss.EVMAddress(), evmSigner.EvmSigner()) + verifyTxSender(t, tx, tss.PubKey().AddressEVM(), evmSigner.EvmSigner()) // Verify tx body basics verifyTxBodyBasics(t, tx, txData.to, txData.nonce, txData.amount) @@ -213,8 +212,8 @@ func TestSigner_SignERC20Withdraw(t *testing.T) { ctx := makeCtx(t) // Setup evm signer - tss := mocks.NewDerivedTSS(chains.BitcoinMainnet) - evmSigner, err := getNewEvmSigner(tss) + tss := mocks.NewTSS(t) + evmSigner, err := getNewEvmSigner(t, tss) require.NoError(t, err) // Setup txData struct @@ -229,7 +228,7 @@ func TestSigner_SignERC20Withdraw(t *testing.T) { require.NoError(t, err) // Verify tx signature - verifyTxSender(t, tx, tss.EVMAddress(), evmSigner.EvmSigner()) + verifyTxSender(t, tx, tss.PubKey().AddressEVM(), evmSigner.EvmSigner()) // Verify tx body basics // Note: Withdraw tx calls erc20 custody contract with 0 gas token diff --git a/zetaclient/chains/evm/signer/signer.go b/zetaclient/chains/evm/signer/signer.go index e666223813..33a1e8656a 100644 --- a/zetaclient/chains/evm/signer/signer.go +++ b/zetaclient/chains/evm/signer/signer.go @@ -186,7 +186,7 @@ func (signer *Signer) Sign( height uint64, ) (*ethtypes.Transaction, []byte, []byte, error) { signer.Logger().Std.Debug(). - Str("tss_pub_key", signer.TSS().EVMAddress().String()). + Str("tss_pub_key", signer.TSS().PubKey().AddressEVM().String()). Msg("Signing evm transaction") chainID := big.NewInt(signer.Chain().ChainId) diff --git a/zetaclient/chains/evm/signer/signer_admin_test.go b/zetaclient/chains/evm/signer/signer_admin_test.go index e5896edcc2..c466b69240 100644 --- a/zetaclient/chains/evm/signer/signer_admin_test.go +++ b/zetaclient/chains/evm/signer/signer_admin_test.go @@ -7,7 +7,6 @@ import ( "github.com/rs/zerolog" "github.com/stretchr/testify/require" - "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/constant" "github.com/zeta-chain/node/testutil/sample" "github.com/zeta-chain/node/zetaclient/testutils/mocks" @@ -17,8 +16,8 @@ func TestSigner_SignAdminTx(t *testing.T) { ctx := makeCtx(t) // Setup evm signer - tss := mocks.NewDerivedTSS(chains.BitcoinMainnet) - evmSigner, err := getNewEvmSigner(tss) + tss := mocks.NewTSS(t) + evmSigner, err := getNewEvmSigner(t, tss) require.NoError(t, err) // Setup txData struct @@ -38,7 +37,7 @@ func TestSigner_SignAdminTx(t *testing.T) { require.NoError(t, err) // Verify tx signature - verifyTxSender(t, tx, tss.EVMAddress(), evmSigner.EvmSigner()) + verifyTxSender(t, tx, tss.PubKey().AddressEVM(), evmSigner.EvmSigner()) // Verify tx body basics // Note: Revert tx calls erc20 custody contract with 0 gas token @@ -58,7 +57,7 @@ func TestSigner_SignAdminTx(t *testing.T) { require.NoError(t, err) // Verify tx signature - verifyTxSender(t, tx, tss.EVMAddress(), evmSigner.EvmSigner()) + verifyTxSender(t, tx, tss.PubKey().AddressEVM(), evmSigner.EvmSigner()) // Verify tx body basics // Note: Revert tx calls erc20 custody contract with 0 gas token @@ -73,7 +72,7 @@ func TestSigner_SignAdminTx(t *testing.T) { require.NoError(t, err) // Verify tx signature - verifyTxSender(t, tx, tss.EVMAddress(), evmSigner.EvmSigner()) + verifyTxSender(t, tx, tss.PubKey().AddressEVM(), evmSigner.EvmSigner()) // Verify tx body basics // Note: Revert tx calls erc20 custody contract with 0 gas token @@ -87,7 +86,7 @@ func TestSigner_SignAdminTx(t *testing.T) { require.NoError(t, err) // Verify tx signature - verifyTxSender(t, tx, tss.EVMAddress(), evmSigner.EvmSigner()) + verifyTxSender(t, tx, tss.PubKey().AddressEVM(), evmSigner.EvmSigner()) // Verify tx body basics verifyTxBodyBasics(t, tx, txData.to, txData.nonce, txData.amount) @@ -98,8 +97,8 @@ func TestSigner_SignWhitelistERC20Cmd(t *testing.T) { ctx := makeCtx(t) // Setup evm signer - tss := mocks.NewDerivedTSS(chains.BitcoinMainnet) - evmSigner, err := getNewEvmSigner(tss) + tss := mocks.NewTSS(t) + evmSigner, err := getNewEvmSigner(t, tss) require.NoError(t, err) // Setup txData struct @@ -119,7 +118,7 @@ func TestSigner_SignWhitelistERC20Cmd(t *testing.T) { require.NotNil(t, tx) // Verify tx signature - verifyTxSender(t, tx, tss.EVMAddress(), evmSigner.EvmSigner()) + verifyTxSender(t, tx, tss.PubKey().AddressEVM(), evmSigner.EvmSigner()) // Verify tx body basics verifyTxBodyBasics(t, tx, txData.to, txData.nonce, zeroValue) @@ -146,8 +145,8 @@ func TestSigner_SignMigrateERC20CustodyFundsCmd(t *testing.T) { ctx := makeCtx(t) // Setup evm signer - tss := mocks.NewDerivedTSS(chains.BitcoinMainnet) - evmSigner, err := getNewEvmSigner(tss) + tss := mocks.NewTSS(t) + evmSigner, err := getNewEvmSigner(t, tss) require.NoError(t, err) // Setup txData struct @@ -175,7 +174,7 @@ func TestSigner_SignMigrateERC20CustodyFundsCmd(t *testing.T) { require.NotNil(t, tx) // Verify tx signature - verifyTxSender(t, tx, tss.EVMAddress(), evmSigner.EvmSigner()) + verifyTxSender(t, tx, tss.PubKey().AddressEVM(), evmSigner.EvmSigner()) // Verify tx body basics verifyTxBodyBasics(t, tx, txData.to, txData.nonce, zeroValue) @@ -211,8 +210,8 @@ func TestSigner_SignUpdateERC20CustodyPauseStatusCmd(t *testing.T) { ctx := makeCtx(t) // Setup evm signer - tss := mocks.NewDerivedTSS(chains.BitcoinMainnet) - evmSigner, err := getNewEvmSigner(tss) + tss := mocks.NewTSS(t) + evmSigner, err := getNewEvmSigner(t, tss) require.NoError(t, err) // Setup txData struct @@ -234,7 +233,7 @@ func TestSigner_SignUpdateERC20CustodyPauseStatusCmd(t *testing.T) { require.NotNil(t, tx) // Verify tx signature - verifyTxSender(t, tx, tss.EVMAddress(), evmSigner.EvmSigner()) + verifyTxSender(t, tx, tss.PubKey().AddressEVM(), evmSigner.EvmSigner()) // Verify tx body basics verifyTxBodyBasics(t, tx, txData.to, txData.nonce, zeroValue) @@ -250,7 +249,7 @@ func TestSigner_SignUpdateERC20CustodyPauseStatusCmd(t *testing.T) { require.NotNil(t, tx) // Verify tx signature - verifyTxSender(t, tx, tss.EVMAddress(), evmSigner.EvmSigner()) + verifyTxSender(t, tx, tss.PubKey().AddressEVM(), evmSigner.EvmSigner()) // Verify tx body basics verifyTxBodyBasics(t, tx, txData.to, txData.nonce, zeroValue) @@ -287,8 +286,8 @@ func TestSigner_SignMigrateTssFundsCmd(t *testing.T) { ctx := makeCtx(t) // Setup evm signer - tss := mocks.NewDerivedTSS(chains.BitcoinMainnet) - evmSigner, err := getNewEvmSigner(tss) + tss := mocks.NewTSS(t) + evmSigner, err := getNewEvmSigner(t, tss) require.NoError(t, err) // Setup txData struct @@ -307,7 +306,7 @@ func TestSigner_SignMigrateTssFundsCmd(t *testing.T) { require.NotNil(t, tx) // Verify tx signature - verifyTxSender(t, tx, tss.EVMAddress(), evmSigner.EvmSigner()) + verifyTxSender(t, tx, tss.PubKey().AddressEVM(), evmSigner.EvmSigner()) // Verify tx body basics verifyTxBodyBasics(t, tx, txData.to, txData.nonce, txData.amount) diff --git a/zetaclient/chains/evm/signer/signer_test.go b/zetaclient/chains/evm/signer/signer_test.go index 43577b475d..cc967d8e3c 100644 --- a/zetaclient/chains/evm/signer/signer_test.go +++ b/zetaclient/chains/evm/signer/signer_test.go @@ -36,12 +36,12 @@ var ( ) // getNewEvmSigner creates a new EVM chain signer for testing -func getNewEvmSigner(tss interfaces.TSSSigner) (*Signer, error) { +func getNewEvmSigner(t *testing.T, tss interfaces.TSSSigner) (*Signer, error) { ctx := context.Background() // use default mock TSS if not provided if tss == nil { - tss = mocks.NewTSSMainnet() + tss = mocks.NewTSS(t) } connectorAddress := ConnectorAddress @@ -67,7 +67,7 @@ func getNewEvmChainObserver(t *testing.T, tss interfaces.TSSSigner) (*observer.O // use default mock TSS if not provided if tss == nil { - tss = mocks.NewTSSMainnet() + tss = mocks.NewTSS(t) } // prepare mock arguments to create observer @@ -136,7 +136,7 @@ func verifyTxBodyBasics( } func TestSigner_SetGetConnectorAddress(t *testing.T) { - evmSigner, err := getNewEvmSigner(nil) + evmSigner, err := getNewEvmSigner(t, nil) require.NoError(t, err) // Get and compare require.Equal(t, ConnectorAddress, evmSigner.GetZetaConnectorAddress()) @@ -148,7 +148,7 @@ func TestSigner_SetGetConnectorAddress(t *testing.T) { } func TestSigner_SetGetERC20CustodyAddress(t *testing.T) { - evmSigner, err := getNewEvmSigner(nil) + evmSigner, err := getNewEvmSigner(t, nil) require.NoError(t, err) // Get and compare require.Equal(t, ERC20CustodyAddress, evmSigner.GetERC20CustodyAddress()) @@ -163,7 +163,7 @@ func TestSigner_TryProcessOutbound(t *testing.T) { ctx := makeCtx(t) // Setup evm signer - evmSigner, err := getNewEvmSigner(nil) + evmSigner, err := getNewEvmSigner(t, nil) require.NoError(t, err) cctx := getCCTX(t) processor := getNewOutboundProcessor() @@ -192,7 +192,7 @@ func TestSigner_BroadcastOutbound(t *testing.T) { ctx := makeCtx(t) // Setup evm signer - evmSigner, err := getNewEvmSigner(nil) + evmSigner, err := getNewEvmSigner(t, nil) require.NoError(t, err) // Setup txData struct diff --git a/zetaclient/chains/solana/observer/observer_test.go b/zetaclient/chains/solana/observer/observer_test.go index 70b3d10090..5e9fdeb4b5 100644 --- a/zetaclient/chains/solana/observer/observer_test.go +++ b/zetaclient/chains/solana/observer/observer_test.go @@ -29,9 +29,10 @@ func MockSolanaObserver( if zetacoreClient == nil { zetacoreClient = mocks.NewZetacoreClient(t).WithKeys(&keys.Keys{}) } + // use mock tss if not provided if tss == nil { - tss = mocks.NewTSSMainnet() + tss = mocks.NewTSS(t) } database, err := db.NewFromSqliteInMemory(true) diff --git a/zetaclient/chains/solana/observer/outbound.go b/zetaclient/chains/solana/observer/outbound.go index 2ed98575c4..b0131c7b77 100644 --- a/zetaclient/chains/solana/observer/outbound.go +++ b/zetaclient/chains/solana/observer/outbound.go @@ -307,8 +307,9 @@ func (ob *Observer) CheckFinalizedTx( } // check tx authorization - if signerECDSA != ob.TSS().EVMAddress() { - logger.Error().Msgf("tx signer %s is not matching current TSS address %s", signerECDSA, ob.TSS().EVMAddress()) + if signerECDSA != ob.TSS().PubKey().AddressEVM() { + logger.Error(). + Msgf("tx signer %s is not matching current TSS address %s", signerECDSA, ob.TSS().PubKey().AddressEVM()) return nil, false } diff --git a/zetaclient/chains/solana/observer/outbound_test.go b/zetaclient/chains/solana/observer/outbound_test.go index bdda96c451..9a6b1ed305 100644 --- a/zetaclient/chains/solana/observer/outbound_test.go +++ b/zetaclient/chains/solana/observer/outbound_test.go @@ -7,6 +7,7 @@ import ( "github.com/gagliardetto/solana-go" "github.com/pkg/errors" + "github.com/rs/zerolog" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/pkg/chains" @@ -35,6 +36,7 @@ const ( // tssAddressTest is the TSS address for testing tssAddressTest = "0x05C7dBdd1954D59c9afaB848dA7d8DD3F35e69Cd" + tssPubKeyTest = "0x0441707acf75468fd132dfe8a4d48a7726adca036199bbacac7be37e9b7104f2b3b69197bbffa6c7e25ba478ba10505c8929a632e4a84dd03e5e04c260e6c52a00" // whitelistTxTest is local devnet tx result for testing whitelistTxTest = "phM9bESbiqojmpkkUxgjed8EABkxvPGNau9q31B8Yk1sXUtsxJvd6G9VbZZQPsEyn6RiTH4YBtqJ89omqfbbNNY" @@ -50,10 +52,13 @@ func createTestObserver( database, err := db.NewFromSqliteInMemory(true) require.NoError(t, err) + testLogger := zerolog.New(zerolog.NewTestWriter(t)) + logger := base.Logger{Std: testLogger, Compliance: testLogger} + // create observer chainParams := sample.ChainParams(chain.ChainId) chainParams.GatewayAddress = GatewayAddressTest - ob, err := observer.NewObserver(chain, solClient, *chainParams, nil, tss, 60, database, base.DefaultLogger(), nil) + ob, err := observer.NewObserver(chain, solClient, *chainParams, nil, tss, 60, database, logger, nil) require.NoError(t, err) return ob @@ -76,7 +81,7 @@ func Test_CheckFinalizedTx(t *testing.T) { solClient.On("GetTransaction", mock.Anything, txSig, mock.Anything).Return(txResult, nil) // mock TSS - tss := mocks.NewMockTSS(chain, tssAddressTest, "") + tss := mocks.NewTSS(t).FakePubKey(tssPubKeyTest) // create observer with and TSS ob := createTestObserver(t, chain, solClient, tss) @@ -132,7 +137,7 @@ func Test_CheckFinalizedTx(t *testing.T) { t.Run("should return error on ECDSA signer mismatch", func(t *testing.T) { // create observer with other TSS address - tssOther := mocks.NewMockTSS(chain, sample.EthAddress().String(), "") + tssOther := mocks.NewTSS(t) ob := createTestObserver(t, chain, solClient, tssOther) tx, finalized := ob.CheckFinalizedTx(ctx, txHash, nonce, coinType) diff --git a/zetaclient/chains/ton/observer/inbound_test.go b/zetaclient/chains/ton/observer/inbound_test.go index e0b7478cfa..8b68e58aff 100644 --- a/zetaclient/chains/ton/observer/inbound_test.go +++ b/zetaclient/chains/ton/observer/inbound_test.go @@ -265,7 +265,7 @@ func TestInbound(t *testing.T) { withdrawalSigner, err := withdrawal.Signer() require.NoError(t, err) - require.Equal(t, ob.TSS().EVMAddress().Hex(), withdrawalSigner.Hex()) + require.Equal(t, ob.TSS().PubKey().AddressEVM().Hex(), withdrawalSigner.Hex()) withdrawalTX := sample.TONWithdrawal(t, ts.gateway.AccountID(), withdrawal) txs := []ton.Transaction{withdrawalTX} diff --git a/zetaclient/chains/ton/observer/observer_test.go b/zetaclient/chains/ton/observer/observer_test.go index 6fc8242e1f..ffbfeb1bd9 100644 --- a/zetaclient/chains/ton/observer/observer_test.go +++ b/zetaclient/chains/ton/observer/observer_test.go @@ -63,7 +63,7 @@ func newTestSuite(t *testing.T) *testSuite { liteClient = mocks.NewLiteClient(t) - tss = mocks.NewGeneratedTSS(t, chain) + tss = mocks.NewTSS(t) zetacore = mocks.NewZetacoreClient(t).WithKeys(&keys.Keys{}) testLogger = zerolog.New(zerolog.NewTestWriter(t)) @@ -207,7 +207,7 @@ func (ts *testSuite) sign(msg signable) { // double check evmSigner, err := msg.Signer() require.NoError(ts.t, err) - require.Equal(ts.t, ts.tss.EVMAddress().String(), evmSigner.String()) + require.Equal(ts.t, ts.tss.PubKey().AddressEVM().String(), evmSigner.String()) } // parses string to TON diff --git a/zetaclient/chains/ton/observer/outbound.go b/zetaclient/chains/ton/observer/outbound.go index deacb8e359..31c859297e 100644 --- a/zetaclient/chains/ton/observer/outbound.go +++ b/zetaclient/chains/ton/observer/outbound.go @@ -175,7 +175,7 @@ func (ob *Observer) determineReceiveStatus(tx *toncontracts.Transaction) (chains switch { case err != nil: return 0, err - case evmSigner != ob.TSS().EVMAddress(): + case evmSigner != ob.TSS().PubKey().AddressEVM(): return 0, errors.New("withdrawal signer is not TSS") case !tx.IsSuccess(): return chains.ReceiveStatus_failed, nil @@ -192,7 +192,7 @@ func (ob *Observer) addOutboundTracker(ctx context.Context, tx *toncontracts.Tra switch { case err != nil: return err - case evmSigner != ob.TSS().EVMAddress(): + case evmSigner != ob.TSS().PubKey().AddressEVM(): ob.Logger().Inbound.Warn(). Fields(txLogFields(tx)). Str("transaction.ton.signer", evmSigner.String()). diff --git a/zetaclient/chains/ton/signer/signer_test.go b/zetaclient/chains/ton/signer/signer_test.go index 9f3a5b7c63..ce0d4fef16 100644 --- a/zetaclient/chains/ton/signer/signer_test.go +++ b/zetaclient/chains/ton/signer/signer_test.go @@ -143,7 +143,7 @@ func newTestSuite(t *testing.T) *testSuite { liteClient = mocks.NewSignerLiteClient(t) - tss = mocks.NewTSSAthens3() + tss = mocks.NewTSS(t) zetacore = mocks.NewZetacoreClient(t).WithKeys(&keys.Keys{}) testLogger = zerolog.New(zerolog.NewTestWriter(t)) diff --git a/zetaclient/orchestrator/bootstap_test.go b/zetaclient/orchestrator/bootstap_test.go index c93efada00..322fbf12e0 100644 --- a/zetaclient/orchestrator/bootstap_test.go +++ b/zetaclient/orchestrator/bootstap_test.go @@ -30,7 +30,7 @@ const ( func TestCreateSignerMap(t *testing.T) { var ( ts = metrics.NewTelemetryServer() - tss = mocks.NewTSSMainnet() + tss = mocks.NewTSS(t) log = zerolog.New(zerolog.NewTestWriter(t)) baseLogger = base.Logger{Std: log, Compliance: log} ) @@ -195,7 +195,7 @@ func TestCreateSignerMap(t *testing.T) { func TestCreateChainObserverMap(t *testing.T) { var ( ts = metrics.NewTelemetryServer() - tss = mocks.NewTSSMainnet() + tss = mocks.NewTSS(t) log = zerolog.New(zerolog.NewTestWriter(t)) baseLogger = base.Logger{Std: log, Compliance: log} client = mocks.NewZetacoreClient(t) diff --git a/zetaclient/testutils/constant.go b/zetaclient/testutils/constant.go index 304cd859c3..d94257ea01 100644 --- a/zetaclient/testutils/constant.go +++ b/zetaclient/testutils/constant.go @@ -10,12 +10,10 @@ const ( // MockEVMRPCEndpoint is the endpoint to enable the mock EVM RPC client MockEVMRPCEndpoint = "MockEVMRPCEnabled" - // TSSAddressEVMMainnet the EVM TSS address for test purposes - // Note: public key is zetapub1addwnpepqtadxdyt037h86z60nl98t6zk56mw5zpnm79tsmvspln3hgt5phdc79kvfc + // TSSAddressEVMMainnet TSSAddressBTCMainnet TSSPubKeyMainnet actual mainnet pub key & addresses TSSAddressEVMMainnet = "0x70e967acFcC17c3941E87562161406d41676FD83" - - // TSSAddressBTCMainnet the BTC TSS address for test purposes TSSAddressBTCMainnet = "bc1qm24wp577nk8aacckv8np465z3dvmu7ry45el6y" + TSSPubKeyMainnet = "zetapub1addwnpepqtadxdyt037h86z60nl98t6zk56mw5zpnm79tsmvspln3hgt5phdc79kvfc" // TSSPubkeyAthens3 is the TSS public key in Athens3 TSSPubkeyAthens3 = "zetapub1addwnpepq28c57cvcs0a2htsem5zxr6qnlvq9mzhmm76z3jncsnzz32rclangr2g35p" diff --git a/zetaclient/testutils/mocks/tss.go b/zetaclient/testutils/mocks/tss.go new file mode 100644 index 0000000000..65ca954e55 --- /dev/null +++ b/zetaclient/testutils/mocks/tss.go @@ -0,0 +1,127 @@ +package mocks + +import ( + "context" + "crypto/ecdsa" + "errors" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/cmd" + zetatss "github.com/zeta-chain/node/zetaclient/tss" +) + +type test = require.TestingT + +type TSS struct { + t test + privateKey *ecdsa.PrivateKey + fakePubKey *zetatss.PubKey + paused bool +} + +func NewTSS(t *testing.T) *TSS { + pk, err := crypto.GenerateKey() + require.NoError(t, err) + + return &TSS{t: t, privateKey: pk} +} + +func NewTSSFromPrivateKey(t test, pk *ecdsa.PrivateKey) *TSS { + return &TSS{t: t, privateKey: pk} +} + +func (tss *TSS) PubKey() zetatss.PubKey { + if tss.fakePubKey != nil { + return *tss.fakePubKey + } + + pubKey, err := zetatss.NewPubKeyFromECDSA(tss.privateKey.PublicKey) + require.NoError(tss.t, err) + + return pubKey +} + +func (tss *TSS) FakePubKey(pk any) *TSS { + if pk == nil { + tss.fakePubKey = nil + return tss + } + + if zpk, ok := pk.(zetatss.PubKey); ok { + tss.fakePubKey = &zpk + return tss + } + + raw, ok := pk.(string) + require.True(tss.t, ok, "invalid type for fake pub key (%v)", pk) + + if strings.HasPrefix(raw, "zetapub") { + zpk, err := zetatss.NewPubKeyFromBech32(raw) + require.NoError(tss.t, err) + tss.fakePubKey = &zpk + return tss + } + + if strings.HasPrefix(raw, "0x") { + zpk, err := zetatss.NewPubKeyFromECDSAHexString(raw) + require.NoError(tss.t, err) + tss.fakePubKey = &zpk + return tss + } + + tss.t.Errorf("invalid fake pub key format: %s", raw) + tss.t.FailNow() + + return nil +} + +func (tss *TSS) Sign(_ context.Context, digest []byte, _, _ uint64, _ int64) ([65]byte, error) { + sigs, err := tss.SignBatch(context.Background(), [][]byte{digest}, 0, 0, 0) + if err != nil { + return [65]byte{}, err + } + + return sigs[0], nil +} + +func (tss *TSS) SignBatch(_ context.Context, digests [][]byte, _, _ uint64, _ int64) ([][65]byte, error) { + // just for backwards compatibility (ideally we should remove this) + if tss.paused { + return nil, errors.New("tss is paused") + } + + sigs := [][65]byte{} + + for _, digest := range digests { + sigBytes, err := crypto.Sign(digest, tss.privateKey) + require.NoError(tss.t, err) + require.Len(tss.t, sigBytes, 65) + + var sig [65]byte + copy(sig[:], sigBytes) + + sigs = append(sigs, sig) + } + + return sigs, nil +} + +func (tss *TSS) UpdatePrivateKey(pk *ecdsa.PrivateKey) { + tss.privateKey = pk +} + +func (tss *TSS) Pause() { + tss.paused = true +} + +func (tss *TSS) Unpause() { + tss.paused = false +} + +func init() { + cmd.SetupCosmosConfig() +} diff --git a/zetaclient/testutils/mocks/tss_signer.go b/zetaclient/testutils/mocks/tss_signer.go deleted file mode 100644 index 89868fc1dd..0000000000 --- a/zetaclient/testutils/mocks/tss_signer.go +++ /dev/null @@ -1,213 +0,0 @@ -package mocks - -import ( - "context" - "crypto/ecdsa" - "fmt" - "testing" - - "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/btcutil" - "github.com/btcsuite/btcd/chaincfg" - ethcommon "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/stretchr/testify/require" - - "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/zetaclient/chains/interfaces" - "github.com/zeta-chain/node/zetaclient/testutils" -) - -// TestPrivateKey is a random private key for testing -var TestPrivateKey *ecdsa.PrivateKey - -// init generates a random private key for testing -func init() { - var err error - TestPrivateKey, err = crypto.GenerateKey() - if err != nil { - fmt.Println(err.Error()) - } -} - -var _ interfaces.TSSSigner = (*TSS)(nil) - -// TSS is a mock of TSS signer for testing -type TSS struct { - paused bool - - // set evmAddress/btcAddress if just want to mock EVMAddress()/BTCAddress() - chain chains.Chain - evmAddress string - btcAddress string - - // set PrivKey if you want to use a specific private key - PrivKey *ecdsa.PrivateKey -} - -func NewMockTSS(chain chains.Chain, evmAddress string, btcAddress string) *TSS { - return &TSS{ - paused: false, - chain: chain, - evmAddress: evmAddress, - btcAddress: btcAddress, - PrivKey: TestPrivateKey, - } -} - -func NewTSSMainnet() *TSS { - return NewMockTSS(chains.BitcoinMainnet, testutils.TSSAddressEVMMainnet, testutils.TSSAddressBTCMainnet) -} - -func NewTSSAthens3() *TSS { - return NewMockTSS(chains.BscTestnet, testutils.TSSAddressEVMAthens3, testutils.TSSAddressBTCAthens3) -} - -// NewDerivedTSS creates a TSS where evmAddress and btcAdresses are always derived from the test -// private key -func NewDerivedTSS(chain chains.Chain) *TSS { - return &TSS{ - paused: false, - chain: chain, - PrivKey: TestPrivateKey, - } -} - -func NewGeneratedTSS(t *testing.T, chain chains.Chain) *TSS { - pk, err := crypto.GenerateKey() - require.NoError(t, err) - - btcPub, err := btcec.ParsePubKey(crypto.FromECDSAPub(&pk.PublicKey)) - require.NoError(t, err) - - btcAddress, err := btcutil.NewAddressWitnessPubKeyHash( - btcutil.Hash160(btcPub.SerializeCompressed()), - &chaincfg.TestNet3Params, - ) - - require.NoError(t, err) - - return &TSS{ - paused: false, - chain: chain, - evmAddress: crypto.PubkeyToAddress(pk.PublicKey).Hex(), - btcAddress: btcAddress.String(), - PrivKey: pk, - } -} - -// WithPrivKey sets the private key for the TSS -func (s *TSS) WithPrivKey(privKey *ecdsa.PrivateKey) *TSS { - s.PrivKey = privKey - return s -} - -// Sign uses test key unrelated to any tss key in production -func (s *TSS) Sign(_ context.Context, data []byte, _ uint64, _ uint64, _ int64) ([65]byte, error) { - // return error if tss is paused - if s.paused { - return [65]byte{}, fmt.Errorf("tss is paused") - } - - signature, err := crypto.Sign(data, s.PrivKey) - if err != nil { - return [65]byte{}, err - } - var sigbyte [65]byte - _ = copy(sigbyte[:], signature[:65]) - - return sigbyte, nil -} - -// SignBatch uses test key unrelated to any tss key in production -func (s *TSS) SignBatch(_ context.Context, _ [][]byte, _ uint64, _ uint64, _ int64) ([][65]byte, error) { - // return error if tss is paused - if s.paused { - return nil, fmt.Errorf("tss is paused") - } - - // mock not implemented yet - return nil, fmt.Errorf("not implemented") -} - -func (s *TSS) Pubkey() []byte { - publicKeyBytes := crypto.FromECDSAPub(&s.PrivKey.PublicKey) - return publicKeyBytes -} - -func (s *TSS) EVMAddress() ethcommon.Address { - // force use evmAddress if set - if s.evmAddress != "" { - return ethcommon.HexToAddress(s.evmAddress) - } - return crypto.PubkeyToAddress(s.PrivKey.PublicKey) -} - -func (s *TSS) EVMAddressList() []ethcommon.Address { - return []ethcommon.Address{s.EVMAddress()} -} - -func (s *TSS) BTCAddress(_ int64) (*btcutil.AddressWitnessPubKeyHash, error) { - // return error if tss is paused - if s.paused { - return nil, fmt.Errorf("tss is paused") - } - - // force use static btcAddress if set - if s.btcAddress != "" { - net, err := chains.GetBTCChainParams(s.chain.ChainId) - if err != nil { - return nil, err - } - addr, err := btcutil.DecodeAddress(s.btcAddress, net) - if err != nil { - return nil, err - } - return addr.(*btcutil.AddressWitnessPubKeyHash), nil - } - // if privkey is set, use it to generate a segwit address - if s.PrivKey != nil { - pkBytes := crypto.FromECDSAPub(&s.PrivKey.PublicKey) - pk, err := btcec.ParsePubKey(pkBytes) - if err != nil { - fmt.Printf("error parsing pubkey: %v", err) - return nil, err - } - - // witness program: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#Witness_program - // The HASH160 of the public key must match the 20-byte witness program. - addrWPKH, err := btcutil.NewAddressWitnessPubKeyHash( - btcutil.Hash160(pk.SerializeCompressed()), - &chaincfg.TestNet3Params, - ) - if err != nil { - fmt.Printf("error NewAddressWitnessPubKeyHash: %v", err) - return nil, err - } - - return addrWPKH, nil - } - return nil, nil -} - -// PubKeyCompressedBytes returns 33B compressed pubkey -func (s *TSS) PubKeyCompressedBytes() []byte { - pkBytes := crypto.FromECDSAPub(&s.PrivKey.PublicKey) - pk, err := btcec.ParsePubKey(pkBytes) - if err != nil { - fmt.Printf("error parsing pubkey: %v", err) - return nil - } - return pk.SerializeCompressed() -} - -// ---------------------------------------------------------------------------- -// methods to control the mock for testing -// ---------------------------------------------------------------------------- -func (s *TSS) Pause() { - s.paused = true -} - -func (s *TSS) Unpause() { - s.paused = false -} diff --git a/zetaclient/tss/crypto.go b/zetaclient/tss/crypto.go index e47bea1027..5d811d25da 100644 --- a/zetaclient/tss/crypto.go +++ b/zetaclient/tss/crypto.go @@ -5,10 +5,12 @@ import ( "crypto/ecdsa" "crypto/elliptic" "encoding/base64" + "encoding/hex" "strings" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" eth "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -54,6 +56,35 @@ func NewPubKeyFromBech32(bech32 string) (PubKey, error) { }, nil } +// NewPubKeyFromECDSA creates a new PubKey from an ECDSA public key. +func NewPubKeyFromECDSA(pk ecdsa.PublicKey) (PubKey, error) { + compressed := elliptic.MarshalCompressed(pk.Curve, pk.X, pk.Y) + + return PubKey{ + cosmosPubKey: &secp256k1.PubKey{Key: compressed}, + ecdsaPubKey: &pk, + }, nil +} + +// NewPubKeyFromECDSAHexString creates PubKey from 0xABC12... +func NewPubKeyFromECDSAHexString(raw string) (PubKey, error) { + if strings.HasPrefix(raw, "0x") { + raw = raw[2:] + } + + b, err := hex.DecodeString(raw) + if err != nil { + return PubKey{}, errors.Wrap(err, "unable to decode hex string") + } + + pk, err := crypto.UnmarshalPubkey(b) + if err != nil { + return PubKey{}, errors.Wrap(err, "unable to unmarshal pubkey") + } + + return NewPubKeyFromECDSA(*pk) +} + // Bytes marshals pubKey to bytes either as compressed or uncompressed slice. // // In ECDSA, a compressed pubKey includes only the X and a parity bit for the Y, diff --git a/zetaclient/tss/crypto_test.go b/zetaclient/tss/crypto_test.go index 4fc493150e..2b14a01e58 100644 --- a/zetaclient/tss/crypto_test.go +++ b/zetaclient/tss/crypto_test.go @@ -36,5 +36,10 @@ func TestPubKey(t *testing.T) { assert.Equal(t, sample, pk.Bech32String()) assert.Equal(t, "0x70e967acfcc17c3941e87562161406d41676fd83", strings.ToLower(addrEVM.Hex())) assert.Equal(t, "bc1qm24wp577nk8aacckv8np465z3dvmu7ry45el6y", addrBTC.String()) + + // Check that NewPubKeyFromECDSA works + pk2, err := NewPubKeyFromECDSA(*pk.ecdsaPubKey) + require.NoError(t, err) + require.Equal(t, pk.Bech32String(), pk2.Bech32String()) }) } From 4dbf3f822ffce9f7839d5e8a5a8b964aff58d2c7 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:30:17 +0100 Subject: [PATCH 16/37] Remove tss historical list from outbound evm cctx --- zetaclient/chains/evm/observer/outbound.go | 39 +++++++------------ .../chains/evm/observer/outbound_test.go | 5 ++- 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/zetaclient/chains/evm/observer/outbound.go b/zetaclient/chains/evm/observer/outbound.go index f8ce8f32ba..adb5ad79b3 100644 --- a/zetaclient/chains/evm/observer/outbound.go +++ b/zetaclient/chains/evm/observer/outbound.go @@ -449,7 +449,7 @@ func (ob *Observer) FilterTSSOutboundInBlock(ctx context.Context, blockNumber ui for i := range block.Transactions { tx := block.Transactions[i] - if ethcommon.HexToAddress(tx.From) == ob.TSS().EVMAddress() { + if ethcommon.HexToAddress(tx.From) == ob.TSS().PubKey().AddressEVM() { // #nosec G115 nonce always positive nonce := uint64(tx.Nonce) if !ob.IsTxConfirmed(nonce) { @@ -501,33 +501,20 @@ func (ob *Observer) checkConfirmedTx( // check tx sender and nonce signer := ethtypes.NewLondonSigner(big.NewInt(ob.Chain().ChainId)) from, err := signer.Sender(transaction) - if err != nil { + switch { + case err != nil: logger.Error().Err(err).Msg("local recovery of sender address failed") return nil, nil, false - } - if from != ob.TSS().EVMAddress() { // must be TSS address - // If from is not TSS address, check if it is one of the previous TSS addresses We can still try to confirm a tx which was broadcast by an old TSS - // This is to handle situations where the outbound has already been broad-casted by an older TSS address and the zetacore is waiting for the all the required block confirmations - // to go through before marking the cctx into a finalized state - - // TODO : improve this logic to verify that the correct TSS address is the from address. - // https://github.com/zeta-chain/node/issues/2487 - logger.Warn(). - Msgf("tx sender %s is not matching current TSS address %s", from.String(), ob.TSS().EVMAddress().String()) - addressList := ob.TSS().EVMAddressList() - isOldTssAddress := false - for _, addr := range addressList { - if from == addr { - isOldTssAddress = true - } - } - if !isOldTssAddress { - logger.Error().Msgf("tx sender %s is not matching any of the TSS addresses", from.String()) - return nil, nil, false - } - } - if transaction.Nonce() != nonce { // must match tracker nonce - logger.Error().Msgf("tx nonce %d is not matching tracker nonce", nonce) + case from != ob.TSS().PubKey().AddressEVM(): + // might be false positive during TSS upgrade for unconfirmed txs + // Make all deposits/withdrawals are paused during TSS upgrade + logger.Error().Str("tx.sender", from.String()).Msgf("tx sender is not TSS addresses") + return nil, nil, false + case transaction.Nonce() != nonce: + logger.Error(). + Uint64("tx.nonce", transaction.Nonce()). + Uint64("tracker.nonce", nonce). + Msg("tx nonce is not matching tracker nonce") return nil, nil, false } diff --git a/zetaclient/chains/evm/observer/outbound_test.go b/zetaclient/chains/evm/observer/outbound_test.go index 5011e5660a..0baa739fd6 100644 --- a/zetaclient/chains/evm/observer/outbound_test.go +++ b/zetaclient/chains/evm/observer/outbound_test.go @@ -442,7 +442,8 @@ func Test_FilterTSSOutbound(t *testing.T) { evmClient.On("TransactionReceipt", mock.Anything, outboundHash).Return(receipt, nil) // create evm observer for testing - tss := mocks.NewTSSMainnet() + tss := mocks.NewTSS(t).FakePubKey(testutils.TSSPubKeyMainnet) + ob, _ := MockEVMObserver(t, chain, evmClient, nil, nil, tss, 1, chainParam) // feed archived block to observer cache @@ -475,7 +476,7 @@ func Test_FilterTSSOutbound(t *testing.T) { evmJSONRPC := mocks.NewMockJSONRPCClient() // create evm observer for testing - tss := mocks.NewTSSMainnet() + tss := mocks.NewTSS(t) ob, _ := MockEVMObserver(t, chain, evmClient, evmJSONRPC, nil, tss, 1, chainParam) // filter TSS outbound From 8c7058c66478f9d4e6f87339899a69341959cee7 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:08:59 +0100 Subject: [PATCH 17/37] TSS healthcheck --- cmd/zetaclientd/start.go | 35 ---------- zetaclient/tss/healthcheck.go | 122 ++++++++++++++++++++++++++++++++++ zetaclient/tss/service.go | 3 + zetaclient/tss/setup.go | 12 +++- 4 files changed, 135 insertions(+), 37 deletions(-) create mode 100644 zetaclient/tss/healthcheck.go diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index 0fd2b264d8..9f428db5d7 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -167,41 +167,6 @@ func Start(_ *cobra.Command, _ []string) error { signalChannel := make(chan os.Signal, 1) signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM) - // todo move to tss/healthcheck.go - //go func() { - // for { - // time.Sleep(30 * time.Second) - // ps := tssServer.GetKnownPeers() - // metrics.NumConnectedPeers.Set(float64(len(ps))) - // telemetryServer.SetConnectedPeers(ps) - // } - //}() - //go func() { - // host := tssServer.GetP2PHost() - // pingRTT := make(map[peer.ID]int64) - // for { - // var wg sync.WaitGroup - // for _, p := range whitelistedPeers { - // wg.Add(1) - // go func(p peer.ID) { - // defer wg.Done() - // ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - // defer cancel() - // result := <-ping.Ping(ctx, host, p) - // if result.Error != nil { - // masterLogger.Error().Err(result.Error).Msg("ping error") - // pingRTT[p] = -1 // RTT -1 indicate ping error - // return - // } - // pingRTT[p] = result.RTT.Nanoseconds() - // }(p) - // } - // wg.Wait() - // telemetryServer.SetPingRTT(pingRTT) - // time.Sleep(30 * time.Second) - // } - //}() - // Starts various background TSS listeners. // Shuts down zetaclientd if any is triggered. maintenance.NewTSSListener(zetacoreClient, masterLogger).Listen(ctx, func() { diff --git a/zetaclient/tss/healthcheck.go b/zetaclient/tss/healthcheck.go new file mode 100644 index 0000000000..e02cb0b722 --- /dev/null +++ b/zetaclient/tss/healthcheck.go @@ -0,0 +1,122 @@ +package tss + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/p2p/protocol/ping" + "github.com/prometheus/client_golang/prometheus" + "github.com/rs/zerolog" + "gitlab.com/thorchain/tss/go-tss/tss" + + "github.com/zeta-chain/node/pkg/bg" + "github.com/zeta-chain/node/pkg/ticker" + "github.com/zeta-chain/node/zetaclient/logs" +) + +// HealthcheckProps represents options for HealthcheckWorker. +type HealthcheckProps struct { + Telemetry Telemetry + Interval time.Duration + WhitelistPeers []peer.ID + NumConnectedPeersMetric prometheus.Gauge +} + +// HealthcheckWorker checks the health of the TSS server and its peers. +func HealthcheckWorker(ctx context.Context, server *tss.TssServer, p HealthcheckProps, logger zerolog.Logger) error { + if p.NumConnectedPeersMetric == nil { + return errors.New("missing NumConnectedPeersMetric") + } + + if p.Interval == 0 { + p.Interval = 30 * time.Second + } + + logger = logger.With().Str(logs.FieldModule, "tss_healthcheck").Logger() + + // Ping & collect round trip time + var ( + host = server.GetP2PHost() + pingRTT = make(map[peer.ID]int64) + mu = sync.Mutex{} + ) + + const pingTimeout = 5 * time.Second + + pinger := func(ctx context.Context, _ *ticker.Ticker) error { + var wg sync.WaitGroup + for i := range p.WhitelistPeers { + peerID := p.WhitelistPeers[i] + if peerID == host.ID() { + continue + } + + wg.Add(1) + + go func() { + defer wg.Done() + + defer func() { + if r := recover(); r != nil { + logger.Error(). + Str("peer_id", peerID.String()). + Interface("panic", r). + Msg("panic during ping") + } + }() + + ctx, cancel := context.WithTimeout(ctx, pingTimeout) + defer cancel() + + result := <-ping.Ping(ctx, host, peerID) + if result.Error != nil { + result.RTT = -1 // indicates ping error + logger.Error().Str("peer_id", peerID.String()).Err(result.Error).Msg("ping error") + } + + mu.Lock() + pingRTT[peerID] = result.RTT.Nanoseconds() + mu.Unlock() + }() + + wg.Wait() + p.Telemetry.SetPingRTT(pingRTT) + } + + return nil + } + + peersCounter := func(_ context.Context, _ *ticker.Ticker) error { + peers := server.GetKnownPeers() + p.NumConnectedPeersMetric.Set(float64(len(peers))) + p.Telemetry.SetConnectedPeers(peers) + + return nil + } + + runBackgroundTicker(ctx, pinger, p.Interval, "TSSHealthcheckPeersPing", logger) + runBackgroundTicker(ctx, peersCounter, p.Interval, "TSSHealthcheckPeersCounter", logger) + + return nil +} + +func runBackgroundTicker( + ctx context.Context, + task ticker.Task, + interval time.Duration, + name string, + logger zerolog.Logger, +) { + bgName := fmt.Sprintf("%sWorker", name) + tickerName := fmt.Sprintf("%sTicker", name) + + bgTask := func(ctx context.Context) error { + return ticker.Run(ctx, interval, task, ticker.WithLogger(logger, tickerName)) + } + + bg.Work(ctx, bgTask, bg.WithName(bgName), bg.WithLogger(logger)) +} diff --git a/zetaclient/tss/service.go b/zetaclient/tss/service.go index 2990e4b4a8..92ab17cdb8 100644 --- a/zetaclient/tss/service.go +++ b/zetaclient/tss/service.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/libp2p/go-libp2p/core/peer" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "github.com/rs/zerolog" @@ -46,6 +47,8 @@ type Zetacore interface { type Telemetry interface { SetP2PID(id string) + SetConnectedPeers(peers []peer.AddrInfo) + SetPingRTT(peers map[peer.ID]int64) } // Service TSS service diff --git a/zetaclient/tss/setup.go b/zetaclient/tss/setup.go index 2a908fbc42..2c2c4f0c82 100644 --- a/zetaclient/tss/setup.go +++ b/zetaclient/tss/setup.go @@ -175,9 +175,17 @@ func Setup(ctx context.Context, p SetupProps, logger zerolog.Logger) (*Service, return nil, errors.Wrap(err, "unable to validate tss addresses") } - logger.Info().Msg("TSS addresses validated") + logger.Info().Msg("TSS addresses validated. Starting healthcheck worker") - // todo health checks + healthCheckProps := HealthcheckProps{ + Telemetry: p.Telemetry, + WhitelistPeers: whitelistedPeers, + NumConnectedPeersMetric: metrics.NumConnectedPeers, + } + + if err = HealthcheckWorker(ctx, tssServer, healthCheckProps, logger); err != nil { + return nil, errors.Wrap(err, "unable to start healthcheck worker") + } return service, nil } From f977b22bc7d29641d1a1771fd27dbd4a253e5131 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 21 Nov 2024 18:17:46 +0100 Subject: [PATCH 18/37] Fix flaky case when GetTSS() returns an empty string --- zetaclient/tss/keygen.go | 50 ++++++++++++++++++++++++++++++++++++---- zetaclient/tss/setup.go | 11 ++------- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/zetaclient/tss/keygen.go b/zetaclient/tss/keygen.go index 19e8a7a233..afb79be3cc 100644 --- a/zetaclient/tss/keygen.go +++ b/zetaclient/tss/keygen.go @@ -33,16 +33,23 @@ type keygenCeremony struct { tss *tss.TssServer zetacore Zetacore lastSeenBlock int64 + iterations int logger zerolog.Logger } // KeygenCeremony runs TSS keygen ceremony as a blocking thread. // Most likely the keygen is already generated, so this function will be a noop. -func KeygenCeremony(ctx context.Context, tssServer *tss.TssServer, zc Zetacore, logger zerolog.Logger) error { +// Returns the TSS key if generated, or error. +func KeygenCeremony( + ctx context.Context, + server *tss.TssServer, + zc Zetacore, + logger zerolog.Logger, +) (observertypes.TSS, error) { const interval = time.Second ceremony := keygenCeremony{ - tss: tssServer, + tss: server, zetacore: zc, logger: logger.With().Str(logs.FieldModule, "tss_keygen").Logger(), } @@ -51,7 +58,7 @@ func KeygenCeremony(ctx context.Context, tssServer *tss.TssServer, zc Zetacore, shouldRetry, err := ceremony.iteration(ctx) switch { case shouldRetry: - if err != nil { + if err != nil && !errors.Is(err, context.Canceled) { logger.Error().Err(err).Msg("Keygen error. Retrying...") } @@ -66,7 +73,20 @@ func KeygenCeremony(ctx context.Context, tssServer *tss.TssServer, zc Zetacore, } } - return ticker.Run(ctx, interval, task, ticker.WithLogger(logger, "tss_keygen")) + err := ticker.Run(ctx, interval, task, ticker.WithLogger(logger, "tss_keygen")) + if err != nil { + return observertypes.TSS{}, err + } + + // If there was only a single iteration, most likely the TSS is already generated, + // Otherwise, we need to wait for the next block to ensure TSS is set by internal keepers. + if ceremony.iterations > 1 { + if err = ceremony.waitForBlock(ctx); err != nil { + return observertypes.TSS{}, errors.Wrap(err, "error waiting for the next block") + } + } + + return zc.GetTSS(ctx) } // iteration runs ceremony iteration every time interval. @@ -75,6 +95,8 @@ func KeygenCeremony(ctx context.Context, tssServer *tss.TssServer, zc Zetacore, // - If the keygen is pending, ensure we're on the right block // - Iteration also ensured that the logic is invoked ONLY once per block (regardless of the interval) func (k *keygenCeremony) iteration(ctx context.Context) (shouldRetry bool, err error) { + k.iterations++ + keygenTask, err := k.zetacore.GetKeyGen(ctx) switch { case err != nil: @@ -219,6 +241,26 @@ func (k *keygenCeremony) blockThrottled(currentBlock int64) bool { } } +func (k *keygenCeremony) waitForBlock(ctx context.Context) error { + height, err := k.zetacore.GetBlockHeight(ctx) + if err != nil { + return errors.Wrap(err, "unable to get block height (initial)") + } + + for { + k.logger.Info().Msg("Waiting for the next block to arrive") + newHeight, err := k.zetacore.GetBlockHeight(ctx) + switch { + case err != nil: + return errors.Wrap(err, "unable to get block height") + case newHeight > height: + return nil + default: + time.Sleep(time.Second) + } + } +} + func digestReq(req keygen.Request) (string, error) { bytes, err := json.Marshal(req) if err != nil { diff --git a/zetaclient/tss/setup.go b/zetaclient/tss/setup.go index 2c2c4f0c82..51badd89b0 100644 --- a/zetaclient/tss/setup.go +++ b/zetaclient/tss/setup.go @@ -120,18 +120,11 @@ func Setup(ctx context.Context, p SetupProps, logger zerolog.Logger) (*Service, logger.Info().Msg("TSS server started") // 5. Perform key generation (if needed) - if err = KeygenCeremony(ctx, tssServer, p.Zetacore, logger); err != nil { - return nil, errors.Wrap(err, "unable to perform keygen ceremony") - } - - // 6. Get tss & tss history from zetacore - tssInfo, err := p.Zetacore.GetTSS(ctx) + tssInfo, err := KeygenCeremony(ctx, tssServer, p.Zetacore, logger) if err != nil { - return nil, errors.Wrap(err, "unable to get TSS from zetacore") + return nil, errors.Wrap(err, "unable to perform keygen ceremony") } - logger.Info().Msg("Got TSS info from zetacore") - historicalTSSInfo, err := p.Zetacore.GetTSSHistory(ctx) if err != nil { return nil, errors.Wrap(err, "unable to get TSS history") From 034f19f13dd9a30c63777a878fd2ba652311e734 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 21 Nov 2024 18:25:43 +0100 Subject: [PATCH 19/37] Update changelog --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index 4facc05f04..7f6274781b 100644 --- a/changelog.md +++ b/changelog.md @@ -20,6 +20,7 @@ * [3125](https://github.com/zeta-chain/node/pull/3125) - drop support for header proofs * [3131](https://github.com/zeta-chain/node/pull/3131) - move app context update from zetacore client * [3137](https://github.com/zeta-chain/node/pull/3137) - remove chain.Chain from zetaclientd config +* [3170](https://github.com/zeta-chain/node/pull/3170) - revamp TSS package in zetaclient ### Fixes * [3117](https://github.com/zeta-chain/node/pull/3117) - register messages for emissions module to legacy amino codec. From 097400dfed8ab030507e372c5f26b3da7f5dd20e Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 21 Nov 2024 19:26:37 +0100 Subject: [PATCH 20/37] Fix typos --- zetaclient/tss/setup.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/zetaclient/tss/setup.go b/zetaclient/tss/setup.go index 51badd89b0..fdc2b0d3b8 100644 --- a/zetaclient/tss/setup.go +++ b/zetaclient/tss/setup.go @@ -98,7 +98,6 @@ func Setup(ctx context.Context, p SetupProps, logger zerolog.Logger) (*Service, logger.Info().Interface("whitelisted_peers", whitelistedPeers).Msg("Resolved whitelist peers") - // todo bump numbers // 4. Bootstrap go-tss TSS server tssServer, err := NewTSSServer( bootstrapPeers, @@ -130,7 +129,7 @@ func Setup(ctx context.Context, p SetupProps, logger zerolog.Logger) (*Service, return nil, errors.Wrap(err, "unable to get TSS history") } - // 7. Verify key shared for public keys + // 6. Verify key shares logger.Info().Msg("Got historical TSS info from zetacore. Verifying key shares...") if err = verifyKeySharesForPubKeys(p, historicalTSSInfo, logger); err != nil { return nil, errors.Wrap(err, "unable to verify key shares for pub keys") @@ -138,7 +137,7 @@ func Setup(ctx context.Context, p SetupProps, logger zerolog.Logger) (*Service, logger.Info().Msg("Key shared verified") - // 8. Optionally test key signing + // 7. Optionally test key signing if p.Config.TestTssKeysign { if err = TestKeySign(tssServer, tssInfo.TssPubkey, logger); err != nil { return nil, errors.Wrap(err, "unable to test key signing") @@ -164,6 +163,7 @@ func Setup(ctx context.Context, p SetupProps, logger zerolog.Logger) (*Service, logger.Info().Msg("TSS service created") + // 9. Ensure that TSS has valid EVM and BTC addresses if err = validateAddresses(service, p.BitcoinChainIDs, logger); err != nil { return nil, errors.Wrap(err, "unable to validate tss addresses") } @@ -176,6 +176,7 @@ func Setup(ctx context.Context, p SetupProps, logger zerolog.Logger) (*Service, NumConnectedPeersMetric: metrics.NumConnectedPeers, } + // 10. Start healthcheck worker if err = HealthcheckWorker(ctx, tssServer, healthCheckProps, logger); err != nil { return nil, errors.Wrap(err, "unable to start healthcheck worker") } From d6cc8415f61ece9dcebdbbeb845bee69e7d6d6e2 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 21 Nov 2024 19:55:34 +0100 Subject: [PATCH 21/37] Forward docker pprof in e2e --- contrib/localnet/docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contrib/localnet/docker-compose.yml b/contrib/localnet/docker-compose.yml index e0078230e7..b58379ae93 100644 --- a/contrib/localnet/docker-compose.yml +++ b/contrib/localnet/docker-compose.yml @@ -111,6 +111,8 @@ services: - HOTKEY_BACKEND=file - HOTKEY_PASSWORD=password # test purposes only restart: always + ports: + - "6061:6061" # pprof volumes: - ssh:/root/.ssh - preparams:/root/preparams From b56bf334b29ca8b76572f36802505a4949a66cb5 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 21 Nov 2024 20:25:54 +0100 Subject: [PATCH 22/37] Forward pprof in e2e docker --- cmd/zetaclientd/start.go | 32 +++++++++++++++++++---------- contrib/localnet/docker-compose.yml | 1 + 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index 4f5c7d4817..7df7e2d6cc 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -3,7 +3,7 @@ package main import ( "context" "net/http" - _ "net/http/pprof" // #nosec G108 -- pprof enablement is intentional + _ "net/http/pprof" // #nosec G108 -- runPprof enablement is intentional "os" "os/signal" "path/filepath" @@ -11,6 +11,7 @@ import ( "syscall" "github.com/pkg/errors" + "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -32,6 +33,7 @@ import ( const ( // enables posting blame data to core for failed TSS signatures envFlagPostBlame = "POST_BLAME" + envPprofAddr = "PPROF_ADDR" ) // Start starts zetaclientd process todo revamp @@ -78,6 +80,8 @@ func Start(_ *cobra.Command, _ []string) error { } }() + go runPprof(startLogger) + // CreateZetacoreClient: zetacore client is used for all communication to zetacore , which this client connects to. // Zetacore accumulates votes , and provides a centralized source of truth for all clients zetacoreClient, err := createZetacoreClient(cfg, hotkeyPass, masterLogger) @@ -169,16 +173,6 @@ func Start(_ *cobra.Command, _ []string) error { signalChannel := make(chan os.Signal, 1) signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM) - // pprof http server - // zetacored/cometbft is already listening for pprof on 6060 (by default) - go func() { - // #nosec G114 -- timeouts uneeded - err := http.ListenAndServe("localhost:6061", nil) - if err != nil { - log.Error().Err(err).Msg("pprof http server error") - } - }() - // Starts various background TSS listeners. // Shuts down zetaclientd if any is triggered. maintenance.NewTSSListener(zetacoreClient, masterLogger).Listen(ctx, func() { @@ -296,3 +290,19 @@ func resolveObserverPubKeyBech32(cfg config.Config, hotKeyPassword string) (stri return granteePubKeyBech32, nil } + +// runPprof run pprof http server +// zetacored/cometbft is already listening for runPprof on 6060 (by default) +func runPprof(logger zerolog.Logger) { + addr := os.Getenv(envPprofAddr) + if addr == "" { + addr = "localhost:6061" + } + + logger.Info().Str("addr", addr).Msg("starting pprof http server") + + // #nosec G114 -- timeouts unneeded + if err := http.ListenAndServe(addr, nil); err != nil { + logger.Error().Err(err).Msg("pprof http server error") + } +} diff --git a/contrib/localnet/docker-compose.yml b/contrib/localnet/docker-compose.yml index b58379ae93..898d931b1b 100644 --- a/contrib/localnet/docker-compose.yml +++ b/contrib/localnet/docker-compose.yml @@ -110,6 +110,7 @@ services: - ETHDEV_ENDPOINT=http://eth:8545 - HOTKEY_BACKEND=file - HOTKEY_PASSWORD=password # test purposes only + - PPROF_ADDR=0.0.0.0:6061 restart: always ports: - "6061:6061" # pprof From a9229ae1a54f88280b3b16c20d332a58d296d2d3 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 21 Nov 2024 20:26:43 +0100 Subject: [PATCH 23/37] Simplify VerifySignature --- zetaclient/tss/crypto.go | 19 ++++++------------- zetaclient/tss/keygen.go | 9 +++++++-- zetaclient/tss/service.go | 6 ++---- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/zetaclient/tss/crypto.go b/zetaclient/tss/crypto.go index 5d811d25da..180f1175c9 100644 --- a/zetaclient/tss/crypto.go +++ b/zetaclient/tss/crypto.go @@ -124,34 +124,27 @@ func (k PubKey) AddressEVM() eth.Address { // VerifySignature checks that keysign.Signature is valid and origins from expected TSS public key. // Also returns signature as [65]byte (R, S, V) -func VerifySignature(sig keysign.Signature, tssPubKey string, expectedMsgHash []byte) ([65]byte, error) { +func VerifySignature(sig keysign.Signature, pk PubKey, hash []byte) ([65]byte, error) { // Check that msg hash equals msg hash in the signature actualMsgHash, err := base64DecodeString(sig.Msg) switch { case err != nil: return [65]byte{}, errors.Wrap(err, "unable to decode message hash") - case !bytes.Equal(expectedMsgHash, actualMsgHash): + case !bytes.Equal(hash, actualMsgHash): return [65]byte{}, errors.New("message hash mismatch") } - // Prepare expected public key - expectedPubKey, err := cosmos.GetPubKeyFromBech32(cosmos.Bech32PubKeyTypeAccPub, tssPubKey) - if err != nil { - return [65]byte{}, errors.Wrap(err, "unable to decode tss pub key from bech32") - } - sigBytes, err := SignatureToBytes(sig) if err != nil { return [65]byte{}, errors.Wrap(err, "unable to convert signature to bytes") } // Recover public key from signature - actualPubKey, err := crypto.SigToPub(expectedMsgHash, sigBytes[:]) - if err != nil { + actualPubKey, err := crypto.SigToPub(hash, sigBytes[:]) + switch { + case err != nil: return [65]byte{}, errors.Wrap(err, "unable to recover public key from signature") - } - - if !bytes.Equal(expectedPubKey.Bytes(), crypto.CompressPubkey(actualPubKey)) { + case crypto.PubkeyToAddress(*actualPubKey) != pk.AddressEVM(): return [65]byte{}, errors.New("public key mismatch") } diff --git a/zetaclient/tss/keygen.go b/zetaclient/tss/keygen.go index afb79be3cc..39bfe8645f 100644 --- a/zetaclient/tss/keygen.go +++ b/zetaclient/tss/keygen.go @@ -277,9 +277,14 @@ func digestReq(req keygen.Request) (string, error) { var testKeySignData = []byte("hello meta") // TestKeySign performs a TSS key-sign test of sample data. -func TestKeySign(keySigner KeySigner, tssPubKey string, logger zerolog.Logger) error { +func TestKeySign(keySigner KeySigner, tssPubKeyBec32 string, logger zerolog.Logger) error { logger = logger.With().Str(logs.FieldModule, "tss_keysign").Logger() + tssPubKey, err := NewPubKeyFromBech32(tssPubKeyBec32) + if err != nil { + return errors.Wrap(err, "unable to parse TSS public key") + } + hashedData := crypto.Keccak256Hash(testKeySignData) logger.Info(). @@ -288,7 +293,7 @@ func TestKeySign(keySigner KeySigner, tssPubKey string, logger zerolog.Logger) e Msg("Performing TSS key-sign test") req := keysign.NewRequest( - tssPubKey, + tssPubKey.Bech32String(), []string{base64.StdEncoding.EncodeToString(hashedData.Bytes())}, 10, nil, diff --git a/zetaclient/tss/service.go b/zetaclient/tss/service.go index 92ab17cdb8..a1cac0b364 100644 --- a/zetaclient/tss/service.go +++ b/zetaclient/tss/service.go @@ -187,11 +187,9 @@ func (s *Service) SignBatch( digestsBase64[i] = base64EncodeString(digest) } - tssPubKeyBech32 := s.PubKey().Bech32String() - // #nosec G115 always in range req := keysign.NewRequest( - tssPubKeyBech32, + s.PubKey().Bech32String(), digestsBase64, int64(height), nil, @@ -215,7 +213,7 @@ func (s *Service) SignBatch( signatures := make([][65]byte, len(res.Signatures)) for i, sigResponse := range res.Signatures { - signatures[i], err = VerifySignature(sigResponse, tssPubKeyBech32, digests[i]) + signatures[i], err = VerifySignature(sigResponse, s.PubKey(), digests[i]) if err != nil { return nil, fmt.Errorf("unable to verify signature: %w (#%d)", err, i) } From 8eba16d6d23f14069a169a24bbb5b9fe7ce7ad7b Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 21 Nov 2024 21:56:49 +0100 Subject: [PATCH 24/37] Fix typo --- cmd/zetaclientd/start.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index 7df7e2d6cc..7cee3fb3fc 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -302,7 +302,8 @@ func runPprof(logger zerolog.Logger) { logger.Info().Str("addr", addr).Msg("starting pprof http server") // #nosec G114 -- timeouts unneeded - if err := http.ListenAndServe(addr, nil); err != nil { + err := http.ListenAndServe(addr, nil) + if err != nil { logger.Error().Err(err).Msg("pprof http server error") } } From f88aca1fbdc8f82c23d65d19df9f351907948497 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 21 Nov 2024 21:57:03 +0100 Subject: [PATCH 25/37] Fix batch signature verification --- zetaclient/tss/crypto.go | 44 ++++++++++++++++++++++++++++++++++ zetaclient/tss/service.go | 11 ++++----- zetaclient/tss/service_test.go | 34 ++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/zetaclient/tss/crypto.go b/zetaclient/tss/crypto.go index 180f1175c9..3ecdec084e 100644 --- a/zetaclient/tss/crypto.go +++ b/zetaclient/tss/crypto.go @@ -6,6 +6,7 @@ import ( "crypto/elliptic" "encoding/base64" "encoding/hex" + "fmt" "strings" "github.com/btcsuite/btcd/btcutil" @@ -168,6 +169,49 @@ func SignatureToBytes(input keysign.Signature) (sig [65]byte, err error) { return sig, nil } +// apparently go-tss returns res.Signatures in a different order than digests, +// thus we need to ensure the order AND verify the signatures +func verifySignatures(digests [][]byte, res keysign.Response, pk PubKey) ([][65]byte, error) { + switch { + case len(digests) == 0: + return nil, errors.New("empty digests list") + case len(digests) != len(res.Signatures): + return nil, errors.New("length mismatch") + case len(digests) == 1: + // most common case + sig, err := VerifySignature(res.Signatures[0], pk, digests[0]) + if err != nil { + return nil, err + } + + return [][65]byte{sig}, nil + } + + // map bas64(digest) => slice index + cache := make(map[string]int, len(digests)) + for i, digest := range digests { + cache[base64EncodeString(digest)] = i + } + + signatures := make([][65]byte, len(res.Signatures)) + + for _, sigResponse := range res.Signatures { + i, ok := cache[sigResponse.Msg] + if !ok { + return nil, errors.Errorf("missing digest %s", sigResponse.Msg) + } + + sig, err := VerifySignature(sigResponse, pk, digests[i]) + if err != nil { + return nil, fmt.Errorf("unable to verify signature: %w (#%d)", err, i) + } + + signatures[i] = sig + } + + return signatures, nil +} + // combineDigests combines the digests func combineDigests(digestList []string) []byte { digestConcat := strings.Join(digestList, "") diff --git a/zetaclient/tss/service.go b/zetaclient/tss/service.go index a1cac0b364..77f7e46e8d 100644 --- a/zetaclient/tss/service.go +++ b/zetaclient/tss/service.go @@ -211,17 +211,14 @@ func (s *Service) SignBatch( return nil, fmt.Errorf("keysign fail: signature list length mismatch") } - signatures := make([][65]byte, len(res.Signatures)) - for i, sigResponse := range res.Signatures { - signatures[i], err = VerifySignature(sigResponse, s.PubKey(), digests[i]) - if err != nil { - return nil, fmt.Errorf("unable to verify signature: %w (#%d)", err, i) - } + sigs, err := verifySignatures(digests, res, s.PubKey()) + if err != nil { + return nil, errors.Wrap(err, "unable to verify signatures") } // todo sig save to LRU cache (chain-id + digest). We need LRU per EACH chain - return signatures, nil + return sigs, nil } var ( diff --git a/zetaclient/tss/service_test.go b/zetaclient/tss/service_test.go index e452a343b8..e3770c88ee 100644 --- a/zetaclient/tss/service_test.go +++ b/zetaclient/tss/service_test.go @@ -81,6 +81,37 @@ func TestService(t *testing.T) { require.NotEmpty(t, sig) }) }) + + t.Run("SignBatch", func(t *testing.T) { + // ARRANGE + ts := newTestSuite(t) + + // Given tss service + s, err := tss.NewService(ts, ts.PubKeyBech32(), ts.zetacore, ts.logger) + require.NoError(t, err) + + // Given several sample messages to sign + digests := [][]byte{ + ts.SampleDigest(), + ts.SampleDigest(), + ts.SampleDigest(), + ts.SampleDigest(), + ts.SampleDigest(), + ts.SampleDigest(), + ts.SampleDigest(), + } + + // Given mock response + const blockHeight = 123 + ts.keySignerMock.AddCall(ts.PubKeyBech32(), digests, blockHeight, true, nil) + + // ACT + sig, err := s.SignBatch(ts.ctx, digests, blockHeight, 2, 3) + + // ASSERT + require.NoError(t, err) + require.NotEmpty(t, sig) + }) } type testSuite struct { @@ -188,6 +219,9 @@ func (m *keySignerMock) sign(req keysign.Request) keysign.Response { }) } + // might be random... we should tolerate that + signatures = lo.Shuffle(signatures) + return keysign.Response{ Signatures: signatures, Status: tsscommon.Success, From 18b7acc078a5fa635ac79810dd2b4b105620d4cd Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 21 Nov 2024 21:57:09 +0100 Subject: [PATCH 26/37] Add readme --- zetaclient/tss/readme.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 zetaclient/tss/readme.md diff --git a/zetaclient/tss/readme.md b/zetaclient/tss/readme.md new file mode 100644 index 0000000000..c98b384037 --- /dev/null +++ b/zetaclient/tss/readme.md @@ -0,0 +1,35 @@ +# Zetaclient TSS Overview + +(Threshold Signature Scheme) + +This package wraps the go-tss library, providing a high-level API for signing arbitrary digests using TSS. +The underlying go-tss library relies on tss-lib. + +## What is a Digest? + +A digest is simply a byte slice (`[]byte`), typically representing a transaction hash or other cryptographic input. +The API allows secure signing of these digests in a distributed manner. + +## Architecture Overview + +This is the approximate structure of the TSS implementation within Zetaclient: + +```text +zetaclientd( + tss.Service( + gotss.Server(libp2p()) + ) +) +``` + +## Package Structure + +- `setup.go`: Initializes the go-tss TSS server and the **Service** wrapper of this package. +- `keygen.go`: Manages the key generation ceremony, creating keys used by TSS. +- `service.go`: Implements the **Service** struct, offering methods for signing and verifying digests. +- Other Files: Utilities and supporting tools for TSS operations. + +## Links + +- `go-tss`: https://github.com/zeta-chain/go-tss +- `tss-lib`: https://github.com/zeta-chain/tss-lib From b5c367a169efcc394116f0d7f20752e16bbe5a21 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Thu, 21 Nov 2024 21:59:24 +0100 Subject: [PATCH 27/37] Fix typo --- cmd/zetaclientd/start.go | 2 +- zetaclient/chains/evm/observer/outbound.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index 7cee3fb3fc..76a3c0808c 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -3,7 +3,7 @@ package main import ( "context" "net/http" - _ "net/http/pprof" // #nosec G108 -- runPprof enablement is intentional + _ "net/http/pprof" // #nosec G108 -- pprof enablement is intentional "os" "os/signal" "path/filepath" diff --git a/zetaclient/chains/evm/observer/outbound.go b/zetaclient/chains/evm/observer/outbound.go index adb5ad79b3..2998f094dc 100644 --- a/zetaclient/chains/evm/observer/outbound.go +++ b/zetaclient/chains/evm/observer/outbound.go @@ -507,7 +507,7 @@ func (ob *Observer) checkConfirmedTx( return nil, nil, false case from != ob.TSS().PubKey().AddressEVM(): // might be false positive during TSS upgrade for unconfirmed txs - // Make all deposits/withdrawals are paused during TSS upgrade + // Make sure all deposits/withdrawals are paused during TSS upgrade logger.Error().Str("tx.sender", from.String()).Msgf("tx sender is not TSS addresses") return nil, nil, false case transaction.Nonce() != nonce: From 3a856c2e247e571911c9475d4160db190af01456 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:20:48 +0100 Subject: [PATCH 28/37] Fix typos in healthcheck --- zetaclient/tss/healthcheck.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/zetaclient/tss/healthcheck.go b/zetaclient/tss/healthcheck.go index e02cb0b722..ea23b17de3 100644 --- a/zetaclient/tss/healthcheck.go +++ b/zetaclient/tss/healthcheck.go @@ -49,15 +49,14 @@ func HealthcheckWorker(ctx context.Context, server *tss.TssServer, p Healthcheck pinger := func(ctx context.Context, _ *ticker.Ticker) error { var wg sync.WaitGroup - for i := range p.WhitelistPeers { - peerID := p.WhitelistPeers[i] + for _, peerID := range p.WhitelistPeers { if peerID == host.ID() { continue } wg.Add(1) - go func() { + go func(peerID peer.ID) { defer wg.Done() defer func() { @@ -81,12 +80,12 @@ func HealthcheckWorker(ctx context.Context, server *tss.TssServer, p Healthcheck mu.Lock() pingRTT[peerID] = result.RTT.Nanoseconds() mu.Unlock() - }() - - wg.Wait() - p.Telemetry.SetPingRTT(pingRTT) + }(peerID) } + wg.Wait() + p.Telemetry.SetPingRTT(pingRTT) + return nil } From 1471f12269ef1f4fcea436316b1874b5bef1957c Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:22:04 +0100 Subject: [PATCH 29/37] Address PR comments [1] --- zetaclient/tss/setup.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/zetaclient/tss/setup.go b/zetaclient/tss/setup.go index fdc2b0d3b8..ae1ff88de7 100644 --- a/zetaclient/tss/setup.go +++ b/zetaclient/tss/setup.go @@ -4,7 +4,7 @@ import ( "context" "fmt" "os" - "path" + "path/filepath" "slices" "time" @@ -52,8 +52,6 @@ func Setup(ctx context.Context, p SetupProps, logger zerolog.Logger) (*Service, return nil, fmt.Errorf("hot privateKey: expect 32 bytes, got %d bytes", len(hotPrivateKey.Bytes())) } - p.Zetacore.GetKeys().GetKeybase() - hotPrivateKeyECDSA := secp256k1.PrivKey(hotPrivateKey.Bytes()[:32]) // 1. Parse bootstrap peer if provided @@ -263,7 +261,7 @@ func resolveTSSPath(tssPath string, logger zerolog.Logger) (string, error) { return "", errors.Wrap(err, "unable to get user home dir") } - tssPath = path.Join(home, ".tss") + tssPath = filepath.Join(home, ".tss") logger.Warn().Msgf("TSS path is empty, falling back to %s", tssPath) return tssPath, nil From 44d75a4dc86fe60c788962e98892b6ee596d37f1 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:31:21 +0100 Subject: [PATCH 30/37] Address PR comments [2] --- zetaclient/chains/bitcoin/signer/signer_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/signer/signer_test.go b/zetaclient/chains/bitcoin/signer/signer_test.go index 76926faa2b..131fbe963f 100644 --- a/zetaclient/chains/bitcoin/signer/signer_test.go +++ b/zetaclient/chains/bitcoin/signer/signer_test.go @@ -229,7 +229,7 @@ func (s *BTCSignerSuite) TestP2WPH(c *C) { func TestAddWithdrawTxOutputs(t *testing.T) { // Create test signer and receiver address signer, err := NewSigner( - chains.BitcoinRegtest, + chains.BitcoinMainnet, mocks.NewTSS(t).FakePubKey(testutils.TSSPubKeyMainnet), nil, base.DefaultLogger(), From 21bc93e12a79947eb42a3e108295c1661e68e629 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:41:37 +0100 Subject: [PATCH 31/37] Address PR comments [3] (improve errors clarity) --- zetaclient/tss/crypto.go | 10 +++++++--- zetaclient/tss/service.go | 6 +++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/zetaclient/tss/crypto.go b/zetaclient/tss/crypto.go index 3ecdec084e..011df48455 100644 --- a/zetaclient/tss/crypto.go +++ b/zetaclient/tss/crypto.go @@ -132,7 +132,7 @@ func VerifySignature(sig keysign.Signature, pk PubKey, hash []byte) ([65]byte, e case err != nil: return [65]byte{}, errors.Wrap(err, "unable to decode message hash") case !bytes.Equal(hash, actualMsgHash): - return [65]byte{}, errors.New("message hash mismatch") + return [65]byte{}, errors.Errorf("message hash mismatch (got 0x%x, want 0x%x)", actualMsgHash, hash) } sigBytes, err := SignatureToBytes(sig) @@ -146,7 +146,11 @@ func VerifySignature(sig keysign.Signature, pk PubKey, hash []byte) ([65]byte, e case err != nil: return [65]byte{}, errors.Wrap(err, "unable to recover public key from signature") case crypto.PubkeyToAddress(*actualPubKey) != pk.AddressEVM(): - return [65]byte{}, errors.New("public key mismatch") + return [65]byte{}, errors.Errorf( + "public key mismatch (got %s, want %s)", + crypto.PubkeyToAddress(*actualPubKey), + pk.AddressEVM(), + ) } return sigBytes, nil @@ -176,7 +180,7 @@ func verifySignatures(digests [][]byte, res keysign.Response, pk PubKey) ([][65] case len(digests) == 0: return nil, errors.New("empty digests list") case len(digests) != len(res.Signatures): - return nil, errors.New("length mismatch") + return nil, errors.Errorf("length mismatch (got %d, want %d)", len(res.Signatures), len(digests)) case len(digests) == 1: // most common case sig, err := VerifySignature(res.Signatures[0], pk, digests[0]) diff --git a/zetaclient/tss/service.go b/zetaclient/tss/service.go index 77f7e46e8d..7a8391ff89 100644 --- a/zetaclient/tss/service.go +++ b/zetaclient/tss/service.go @@ -208,7 +208,11 @@ func (s *Service) SignBatch( case len(res.Signatures) == 0: return nil, fmt.Errorf("keysign fail: signature list is empty") case len(res.Signatures) != len(digests): - return nil, fmt.Errorf("keysign fail: signature list length mismatch") + return nil, fmt.Errorf( + "keysign fail: signatures length mismatch (got %d, want %d)", + len(res.Signatures), + len(digests), + ) } sigs, err := verifySignatures(digests, res, s.PubKey()) From e3f43d86ed5b475643255dfc57b681d6c92dcb2a Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:04:18 +0100 Subject: [PATCH 32/37] Address PR comments [4] --- changelog.md | 6 +++++- zetaclient/metrics/metrics.go | 6 +++--- zetaclient/tss/config.go | 1 + zetaclient/tss/keygen.go | 2 +- zetaclient/tss/setup.go | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/changelog.md b/changelog.md index 0e720f544a..4915542791 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,10 @@ # CHANGELOG +## Unreleased + +## Refactor +* [3170](https://github.com/zeta-chain/node/pull/3170) - revamp TSS package in zetaclient + ## v23.0.0 ### Features @@ -25,7 +30,6 @@ * [3125](https://github.com/zeta-chain/node/pull/3125) - drop support for header proofs * [3131](https://github.com/zeta-chain/node/pull/3131) - move app context update from zetacore client * [3137](https://github.com/zeta-chain/node/pull/3137) - remove chain.Chain from zetaclientd config -* [3170](https://github.com/zeta-chain/node/pull/3170) - revamp TSS package in zetaclient ### Fixes diff --git a/zetaclient/metrics/metrics.go b/zetaclient/metrics/metrics.go index a826da58f2..ddd8b0aa3f 100644 --- a/zetaclient/metrics/metrics.go +++ b/zetaclient/metrics/metrics.go @@ -43,11 +43,11 @@ var ( Help: "Count of getLogs per chain", }, []string{"chain"}) - // TssNodeBlamePerPubKey is a counter that contains the number of tss node blame per pubkey - TssNodeBlamePerPubKey = promauto.NewCounterVec(prometheus.CounterOpts{ + // TSSNodeBlamePerPubKey is a counter that contains the number of tss node blame per pubkey + TSSNodeBlamePerPubKey = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: ZetaClientNamespace, Name: "tss_node_blame_count", - Help: "Tss node blame counter per pubkey", + Help: "TSS node blame counter per pubkey", }, []string{"pubkey"}) // RelayerKeyBalance is a gauge that contains the relayer key balance of the chain diff --git a/zetaclient/tss/config.go b/zetaclient/tss/config.go index 3881c96e3a..fcf89ce7cf 100644 --- a/zetaclient/tss/config.go +++ b/zetaclient/tss/config.go @@ -14,6 +14,7 @@ import ( ) const ( + // Port is the default port for go-tss server. Port = 6668 Version = "0.14.0" Algo = tsscommon.ECDSA diff --git a/zetaclient/tss/keygen.go b/zetaclient/tss/keygen.go index 39bfe8645f..dc2a120262 100644 --- a/zetaclient/tss/keygen.go +++ b/zetaclient/tss/keygen.go @@ -207,7 +207,7 @@ func (k *keygenCeremony) performKeygen(ctx context.Context, keygenTask observert // increment blame counter for _, node := range res.Blame.BlameNodes { - metrics.TssNodeBlamePerPubKey.WithLabelValues(node.Pubkey).Inc() + metrics.TSSNodeBlamePerPubKey.WithLabelValues(node.Pubkey).Inc() } blameDigest, err := digestReq(req) diff --git a/zetaclient/tss/setup.go b/zetaclient/tss/setup.go index ae1ff88de7..9f9343beb7 100644 --- a/zetaclient/tss/setup.go +++ b/zetaclient/tss/setup.go @@ -152,7 +152,7 @@ func Setup(ctx context.Context, p SetupProps, logger zerolog.Logger) (*Service, WithMetrics(ctx, p.Zetacore, &Metrics{ ActiveMsgsSigns: metrics.NumActiveMsgSigns, SignLatency: metrics.SignLatency, - NodeBlamePerPubKey: metrics.TssNodeBlamePerPubKey, + NodeBlamePerPubKey: metrics.TSSNodeBlamePerPubKey, }), ) if err != nil { From 578bf5ec8aa8a2abe99bc49372d8bf40dae38f67 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:12:37 +0100 Subject: [PATCH 33/37] Remove redundant methods from app context --- .../chains/evm/observer/observer_test.go | 2 - zetaclient/chains/evm/signer/signer_test.go | 2 - zetaclient/context/app.go | 57 ++----------------- zetaclient/context/app_test.go | 22 ++----- zetaclient/orchestrator/bootstap_test.go | 2 - zetaclient/orchestrator/contextupdater.go | 14 +---- .../orchestrator/contextupdater_test.go | 2 - zetaclient/orchestrator/orchestrator_test.go | 2 - 8 files changed, 13 insertions(+), 90 deletions(-) diff --git a/zetaclient/chains/evm/observer/observer_test.go b/zetaclient/chains/evm/observer/observer_test.go index 523dff1e4a..e8fc39b9fc 100644 --- a/zetaclient/chains/evm/observer/observer_test.go +++ b/zetaclient/chains/evm/observer/observer_test.go @@ -69,11 +69,9 @@ func getAppContext( // feed chain params err := appContext.Update( - observertypes.Keygen{}, []chains.Chain{evmChain, chains.ZetaChainMainnet}, nil, chainParams, - "tssPubKey", *sample.CrosschainFlags(), ) require.NoError(t, err) diff --git a/zetaclient/chains/evm/signer/signer_test.go b/zetaclient/chains/evm/signer/signer_test.go index cc967d8e3c..4165021f0e 100644 --- a/zetaclient/chains/evm/signer/signer_test.go +++ b/zetaclient/chains/evm/signer/signer_test.go @@ -251,13 +251,11 @@ func makeCtx(t *testing.T) context.Context { bscParams := mocks.MockChainParams(chains.BscMainnet.ChainId, 10) err := app.Update( - observertypes.Keygen{}, []chains.Chain{chains.BscMainnet, chains.ZetaChainMainnet}, nil, map[int64]*observertypes.ChainParams{ chains.BscMainnet.ChainId: &bscParams, }, - "tssPubKey", observertypes.CrosschainFlags{}, ) require.NoError(t, err, "unable to update app context") diff --git a/zetaclient/context/app.go b/zetaclient/context/app.go index 8474c3ec87..d796c5beec 100644 --- a/zetaclient/context/app.go +++ b/zetaclient/context/app.go @@ -22,20 +22,14 @@ type AppContext struct { // config is the config of the app config config.Config - // logger is the logger of the app - logger zerolog.Logger - // chainRegistry is a registry of supported chains chainRegistry *ChainRegistry - // currentTssPubKey is the current tss pubKey - currentTssPubKey string - // crosschainFlags is the current crosschain flags state crosschainFlags observertypes.CrosschainFlags - // keygen is the current tss keygen state - keygen observertypes.Keygen + // logger is the logger of the app + logger zerolog.Logger mu sync.RWMutex } @@ -43,16 +37,10 @@ type AppContext struct { // New creates and returns new empty AppContext func New(cfg config.Config, relayerKeyPasswords map[string]string, logger zerolog.Logger) *AppContext { return &AppContext{ - config: cfg, - logger: logger.With().Str("module", "appcontext").Logger(), - - chainRegistry: NewChainRegistry(relayerKeyPasswords), - - crosschainFlags: observertypes.CrosschainFlags{}, - currentTssPubKey: "", - keygen: observertypes.Keygen{}, - - mu: sync.RWMutex{}, + config: cfg, + chainRegistry: NewChainRegistry(relayerKeyPasswords), + crosschainFlags: observertypes.CrosschainFlags{}, + logger: logger.With().Str("module", "appcontext").Logger(), } } @@ -102,32 +90,6 @@ func (a *AppContext) IsInboundObservationEnabled() bool { return a.GetCrossChainFlags().IsInboundEnabled } -// GetKeygen returns the current keygen -func (a *AppContext) GetKeygen() observertypes.Keygen { - a.mu.RLock() - defer a.mu.RUnlock() - - var copiedPubKeys []string - if a.keygen.GranteePubkeys != nil { - copiedPubKeys = make([]string, len(a.keygen.GranteePubkeys)) - copy(copiedPubKeys, a.keygen.GranteePubkeys) - } - - return observertypes.Keygen{ - Status: a.keygen.Status, - GranteePubkeys: copiedPubKeys, - BlockNumber: a.keygen.BlockNumber, - } -} - -// GetCurrentTssPubKey returns the current tss pubKey. -func (a *AppContext) GetCurrentTssPubKey() string { - a.mu.RLock() - defer a.mu.RUnlock() - - return a.currentTssPubKey -} - // GetCrossChainFlags returns crosschain flags func (a *AppContext) GetCrossChainFlags() observertypes.CrosschainFlags { a.mu.RLock() @@ -139,10 +101,8 @@ func (a *AppContext) GetCrossChainFlags() observertypes.CrosschainFlags { // Update updates AppContext and params for all chains // this must be the ONLY function that writes to AppContext func (a *AppContext) Update( - keygen observertypes.Keygen, freshChains, additionalChains []chains.Chain, freshChainParams map[int64]*observertypes.ChainParams, - tssPubKey string, crosschainFlags observertypes.CrosschainFlags, ) error { // some sanity checks @@ -151,9 +111,6 @@ func (a *AppContext) Update( return fmt.Errorf("no chains present") case len(freshChainParams) == 0: return fmt.Errorf("no chain params present") - case tssPubKey == "" && a.currentTssPubKey != "": - // note that if we're doing a fresh start, we ALLOW an empty tssPubKey - return fmt.Errorf("tss pubkey is empty") case len(additionalChains) > 0: for _, c := range additionalChains { if !c.IsExternal { @@ -171,8 +128,6 @@ func (a *AppContext) Update( defer a.mu.Unlock() a.crosschainFlags = crosschainFlags - a.keygen = keygen - a.currentTssPubKey = tssPubKey return nil } diff --git a/zetaclient/context/app_test.go b/zetaclient/context/app_test.go index 2297a3cdec..9df52db1fc 100644 --- a/zetaclient/context/app_test.go +++ b/zetaclient/context/app_test.go @@ -18,17 +18,11 @@ func TestAppContext(t *testing.T) { testCfg = config.New(false) logger = zerolog.New(zerolog.NewTestWriter(t)) - keyGen = types.Keygen{ - Status: types.KeygenStatus_KeyGenSuccess, - GranteePubkeys: []string{"testPubKey1"}, - BlockNumber: 123, - } ccFlags = types.CrosschainFlags{ IsInboundEnabled: true, IsOutboundEnabled: true, GasPriceIncreaseFlags: nil, } - ttsPubKey = "tssPubKeyTest" ) testCfg.BTCChainConfigs[111] = config.BTCConfig{RPCUsername: "satoshi"} @@ -64,8 +58,6 @@ func TestAppContext(t *testing.T) { require.ErrorIs(t, err, ErrChainNotFound) require.Equal(t, testCfg, appContext.Config()) - require.Empty(t, appContext.GetKeygen()) - require.Empty(t, appContext.GetCurrentTssPubKey()) require.Empty(t, appContext.GetCrossChainFlags()) require.False(t, appContext.IsInboundObservationEnabled()) require.False(t, appContext.IsOutboundObservationEnabled()) @@ -89,15 +81,13 @@ func TestAppContext(t *testing.T) { } // ACT - err = appContext.Update(keyGen, newChains, additionalChains, chainParams, ttsPubKey, ccFlags) + err = appContext.Update(newChains, additionalChains, chainParams, ccFlags) // ASSERT require.NoError(t, err) // Check getters assert.Equal(t, testCfg, appContext.Config()) - assert.Equal(t, keyGen, appContext.GetKeygen()) - assert.Equal(t, ttsPubKey, appContext.GetCurrentTssPubKey()) assert.Equal(t, ccFlags, appContext.GetCrossChainFlags()) assert.True(t, appContext.IsInboundObservationEnabled()) assert.True(t, appContext.IsOutboundObservationEnabled()) @@ -132,7 +122,7 @@ func TestAppContext(t *testing.T) { { name: "update with empty chains results in an error", act: func(a *AppContext) error { - return appContext.Update(keyGen, newChains, nil, nil, ttsPubKey, ccFlags) + return appContext.Update(newChains, nil, nil, ccFlags) }, assert: func(t *testing.T, a *AppContext, err error) { assert.ErrorContains(t, err, "no chain params present") @@ -153,7 +143,7 @@ func TestAppContext(t *testing.T) { chainParamsWithOpt := maps.Clone(chainParams) chainParamsWithOpt[opParams.ChainId] = opParams - return a.Update(keyGen, chainsWithOpt, additionalChains, chainParamsWithOpt, ttsPubKey, ccFlags) + return a.Update(chainsWithOpt, additionalChains, chainParamsWithOpt, ccFlags) }, assert: func(t *testing.T, a *AppContext, err error) { assert.ErrorIs(t, err, ErrChainNotSupported) @@ -164,7 +154,7 @@ func TestAppContext(t *testing.T) { name: "trying to add zeta chain without chain params is allowed", act: func(a *AppContext) error { chainsWithZeta := append(newChains, chains.ZetaChainMainnet) - return a.Update(keyGen, chainsWithZeta, additionalChains, chainParams, ttsPubKey, ccFlags) + return a.Update(chainsWithZeta, additionalChains, chainParams, ccFlags) }, assert: func(t *testing.T, a *AppContext, err error) { assert.NoError(t, err) @@ -186,7 +176,7 @@ func TestAppContext(t *testing.T) { chainsWithZeta := append(newChains, chains.ZetaChainMainnet) - return a.Update(keyGen, chainsWithZeta, additionalChains, chainParamsWithZeta, ttsPubKey, ccFlags) + return a.Update(chainsWithZeta, additionalChains, chainParamsWithZeta, ccFlags) }, assert: func(t *testing.T, a *AppContext, err error) { assert.NoError(t, err) @@ -209,7 +199,7 @@ func TestAppContext(t *testing.T) { updatedChainParams[maticParams.ChainId] = maticParams delete(updatedChainParams, chains.ZetaChainMainnet.ChainId) - return a.Update(keyGen, newChains, additionalChains, updatedChainParams, ttsPubKey, ccFlags) + return a.Update(newChains, additionalChains, updatedChainParams, ccFlags) }, assert: func(t *testing.T, a *AppContext, err error) { assert.ErrorContains(t, err, "unable to locate fresh chain 137 based on chain params") diff --git a/zetaclient/orchestrator/bootstap_test.go b/zetaclient/orchestrator/bootstap_test.go index 322fbf12e0..d5f64c8500 100644 --- a/zetaclient/orchestrator/bootstap_test.go +++ b/zetaclient/orchestrator/bootstap_test.go @@ -443,11 +443,9 @@ func mustUpdateAppContext( chainParams map[int64]*observertypes.ChainParams, ) { err := app.Update( - app.GetKeygen(), chains, additionalChains, chainParams, - "tssPubKey", app.GetCrossChainFlags(), ) diff --git a/zetaclient/orchestrator/contextupdater.go b/zetaclient/orchestrator/contextupdater.go index 02e4b275e0..071ded772c 100644 --- a/zetaclient/orchestrator/contextupdater.go +++ b/zetaclient/orchestrator/contextupdater.go @@ -86,21 +86,11 @@ func UpdateAppContext(ctx context.Context, app *zctx.AppContext, zc Zetacore, lo return errors.Wrap(err, "unable to fetch chain params") } - keyGen, err := zc.GetKeyGen(ctx) - if err != nil { - return errors.Wrap(err, "unable to fetch keygen from zetacore") - } - crosschainFlags, err := zc.GetCrosschainFlags(ctx) if err != nil { return errors.Wrap(err, "unable to fetch crosschain flags from zetacore") } - tss, err := zc.GetTSS(ctx) - if err != nil { - return errors.Wrap(err, "unable to fetch current TSS") - } - freshParams := make(map[int64]*observertypes.ChainParams, len(chainParams)) // check and update chain params for each chain @@ -117,7 +107,7 @@ func UpdateAppContext(ctx context.Context, app *zctx.AppContext, zc Zetacore, lo continue } - if err := observertypes.ValidateChainParams(cp); err != nil { + if err = observertypes.ValidateChainParams(cp); err != nil { logger.Warn().Err(err).Int64("chain.id", cp.ChainId).Msg("Skipping invalid chain params") continue } @@ -126,11 +116,9 @@ func UpdateAppContext(ctx context.Context, app *zctx.AppContext, zc Zetacore, lo } return app.Update( - keyGen, supportedChains, additionalChains, freshParams, - tss.GetTssPubkey(), crosschainFlags, ) } diff --git a/zetaclient/orchestrator/contextupdater_test.go b/zetaclient/orchestrator/contextupdater_test.go index cc28d5ad9e..70bef7dd9a 100644 --- a/zetaclient/orchestrator/contextupdater_test.go +++ b/zetaclient/orchestrator/contextupdater_test.go @@ -43,9 +43,7 @@ func Test_UpdateAppContext(t *testing.T) { zetacore.On("GetSupportedChains", mock.Anything).Return(newChains, nil) zetacore.On("GetAdditionalChains", mock.Anything).Return(nil, nil) zetacore.On("GetChainParams", mock.Anything).Return(newParams, nil) - zetacore.On("GetKeyGen", mock.Anything).Return(observertypes.Keygen{}, nil) zetacore.On("GetCrosschainFlags", mock.Anything).Return(ccFlags, nil) - zetacore.On("GetTSS", mock.Anything).Return(observertypes.TSS{TssPubkey: "0x123"}, nil) // ACT err := UpdateAppContext(ctx, app, zetacore, logger) diff --git a/zetaclient/orchestrator/orchestrator_test.go b/zetaclient/orchestrator/orchestrator_test.go index 4b85ccbb03..8637a47e17 100644 --- a/zetaclient/orchestrator/orchestrator_test.go +++ b/zetaclient/orchestrator/orchestrator_test.go @@ -627,11 +627,9 @@ func createAppContext(t *testing.T, chainsOrParams ...any) *zctx.AppContext { // feed chain params err := appContext.Update( - observertypes.Keygen{}, supportedChains, nil, params, - "tssPubKey", *ccFlags, ) require.NoError(t, err, "failed to update app context") From 9b264d9d8816510374ee1b0f5d676021413baeef Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:31:20 +0100 Subject: [PATCH 34/37] Address PR comments [5] --- pkg/cosmos/cosmos.go | 7 ++++--- zetaclient/tss/crypto.go | 4 +--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pkg/cosmos/cosmos.go b/pkg/cosmos/cosmos.go index bcaccacf11..6e62957bc4 100644 --- a/pkg/cosmos/cosmos.go +++ b/pkg/cosmos/cosmos.go @@ -4,8 +4,9 @@ import ( "github.com/cosmos/cosmos-sdk/types/bech32/legacybech32" // nolint ) +const Bech32PubKeyTypeAccPub = legacybech32.AccPK + var ( - GetPubKeyFromBech32 = legacybech32.UnmarshalPubKey - Bech32ifyPubKey = legacybech32.MarshalPubKey - Bech32PubKeyTypeAccPub = legacybech32.AccPK + GetPubKeyFromBech32 = legacybech32.UnmarshalPubKey + Bech32ifyPubKey = legacybech32.MarshalPubKey ) diff --git a/zetaclient/tss/crypto.go b/zetaclient/tss/crypto.go index 011df48455..170f433657 100644 --- a/zetaclient/tss/crypto.go +++ b/zetaclient/tss/crypto.go @@ -104,10 +104,8 @@ func (k PubKey) Bytes(compress bool) []byte { // Example: `zetapub1addwnpepq2fdhcmfyv07s86djjca835l4f2n2ta0c7le6vnl508mseca2s9g6slj0gm` func (k PubKey) Bech32String() string { v, err := cosmos.Bech32ifyPubKey(cosmos.Bech32PubKeyTypeAccPub, k.cosmosPubKey) - - // should not happen as we only set k.cosmosPubKey from the constructor if err != nil { - panic("PubKey.Bech32String: " + err.Error()) + return "" // not possible } return v From 14c2de29bdb8a1ca220f3585323bf476cec3c4b1 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:09:54 +0100 Subject: [PATCH 35/37] Address PR comments [6] (test coverage) --- zetaclient/tss/config.go | 6 +++ zetaclient/tss/config_test.go | 68 +++++++++++++++++++++++++ zetaclient/tss/crypto_test.go | 51 ++++++++++++++++--- zetaclient/tss/service_test.go | 3 -- zetaclient/tss/testdata/pre-params.json | 14 +++++ 5 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 zetaclient/tss/testdata/pre-params.json diff --git a/zetaclient/tss/config.go b/zetaclient/tss/config.go index fcf89ce7cf..06a24e87ed 100644 --- a/zetaclient/tss/config.go +++ b/zetaclient/tss/config.go @@ -11,6 +11,8 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog" tsscommon "gitlab.com/thorchain/tss/go-tss/common" + + "github.com/zeta-chain/node/cmd" ) const ( @@ -20,6 +22,10 @@ const ( Algo = tsscommon.ECDSA ) +func init() { + cmd.SetupCosmosConfig() +} + // MultiAddressFromString parses a string into a slice of addresses (for convenience). func MultiAddressFromString(peer string) ([]multiaddr.Multiaddr, error) { if peer == "" { diff --git a/zetaclient/tss/config_test.go b/zetaclient/tss/config_test.go index e21ab2b2c3..1c220a70f6 100644 --- a/zetaclient/tss/config_test.go +++ b/zetaclient/tss/config_test.go @@ -1,8 +1,10 @@ package tss import ( + _ "embed" "fmt" "os" + "path/filepath" "testing" "github.com/cosmos/cosmos-sdk/testutil/testdata" @@ -40,6 +42,58 @@ func Test_ParsePubKeysFromPath(t *testing.T) { } } +func Test_ResolvePreParamsFromPath(t *testing.T) { + t.Run("file not found", func(t *testing.T) { + // ARRANGE + path := filepath.Join(os.TempDir(), "hello-123.json") + + // ACT + _, err := ResolvePreParamsFromPath(path) + + // ASSERT + require.Error(t, err) + require.Contains(t, err.Error(), "unable to read pre-params") + }) + + t.Run("invalid file", func(t *testing.T) { + // ARRANGE + tmpFile, err := os.CreateTemp(os.TempDir(), "pre-params-*.json") + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, os.Remove(tmpFile.Name())) + }) + + _, err = tmpFile.WriteString(`invalid-json`) + require.NoError(t, err) + tmpFile.Close() + + // ACT + _, err = ResolvePreParamsFromPath(tmpFile.Name()) + + // ASSERT + require.Error(t, err) + require.Contains(t, err.Error(), "unable to decode pre-params") + }) + + t.Run("AllGood", func(t *testing.T) { + // ARRANGE + tmpFile, err := os.CreateTemp(os.TempDir(), "pre-params-*.json") + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, os.Remove(tmpFile.Name())) + }) + + createPreParams(t, tmpFile.Name()) + + // ACT + resolvedPreParams, err := ResolvePreParamsFromPath(tmpFile.Name()) + + // Assert + require.NoError(t, err) + require.NotNil(t, resolvedPreParams) + }) +} + func generateKeyShareFiles(t *testing.T, n int, dir string) { err := os.Chdir(dir) require.NoError(t, err) @@ -61,3 +115,17 @@ func generateKeyShareFiles(t *testing.T, n int, dir string) { require.NoError(t, err) } } + +//go:embed testdata/pre-params.json +var preParamsFixture []byte + +// createPreParams creates a pre-params file at the given path. +// uses fixture to skip long setup. +func createPreParams(t *testing.T, filePath string) { + file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0600) + require.NoError(t, err) + + _, err = file.Write(preParamsFixture) + require.NoError(t, err) + require.NoError(t, file.Close()) +} diff --git a/zetaclient/tss/crypto_test.go b/zetaclient/tss/crypto_test.go index 2b14a01e58..b34e0dc21e 100644 --- a/zetaclient/tss/crypto_test.go +++ b/zetaclient/tss/crypto_test.go @@ -1,24 +1,37 @@ package tss import ( + "encoding/hex" "strings" "testing" + "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zeta-chain/node/cmd" "github.com/zeta-chain/node/pkg/chains" ) func TestPubKey(t *testing.T) { - cmd.SetupCosmosConfig() - t.Run("Invalid", func(t *testing.T) { - _, err := NewPubKeyFromBech32("") - require.ErrorContains(t, err, "empty bech32 address") + cases := []struct { + name string + input string + errMsg string + }{ + {"empty string", "", "empty bech32 address"}, + {"invalid prefix", "invalid1addwnpepq...", "unable to GetPubKeyFromBech32"}, + {"malformed bech32", "zetapub1invalid", "decoding bech32 failed"}, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + _, err := NewPubKeyFromBech32(tt.input) + require.ErrorContains(t, err, tt.errMsg) + }) + } }) - t.Run("Valid", func(t *testing.T) { + t.Run("Valid NewPubKeyFromBech32", func(t *testing.T) { // ARRANGE const sample = `zetapub1addwnpepqtadxdyt037h86z60nl98t6zk56mw5zpnm79tsmvspln3hgt5phdc79kvfc` @@ -42,4 +55,30 @@ func TestPubKey(t *testing.T) { require.NoError(t, err) require.Equal(t, pk.Bech32String(), pk2.Bech32String()) }) + + t.Run("Valid NewPubKeyFromECDSAHexString", func(t *testing.T) { + // ARRANGE + pk, err := crypto.GenerateKey() + require.NoError(t, err) + + pubKeyHex := hex.EncodeToString(crypto.FromECDSAPub(&pk.PublicKey)) + evmAddr := crypto.PubkeyToAddress(pk.PublicKey) + + // ACT + actual, err := NewPubKeyFromECDSAHexString(pubKeyHex) + + // ASSERT + require.NoError(t, err) + assert.Equal(t, evmAddr, actual.AddressEVM()) + assert.True(t, strings.HasPrefix(actual.Bech32String(), "zetapub")) + + t.Run("With 0x prefix", func(t *testing.T) { + // ACT + actual2, err := NewPubKeyFromECDSAHexString("0x" + pubKeyHex) + + // ASSERT + require.NoError(t, err) + assert.Equal(t, actual.Bech32String(), actual2.Bech32String()) + }) + }) } diff --git a/zetaclient/tss/service_test.go b/zetaclient/tss/service_test.go index e3770c88ee..e0ccde6954 100644 --- a/zetaclient/tss/service_test.go +++ b/zetaclient/tss/service_test.go @@ -15,7 +15,6 @@ import ( "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zeta-chain/node/cmd" "github.com/zeta-chain/node/pkg/cosmos" "github.com/zeta-chain/node/zetaclient/testutils/mocks" "github.com/zeta-chain/node/zetaclient/tss" @@ -30,8 +29,6 @@ var ( ) func TestService(t *testing.T) { - cmd.SetupCosmosConfig() - t.Run("NewService", func(t *testing.T) { t.Run("Invalid pub key", func(t *testing.T) { s, err := tss.NewService(nil, "hello", nil, zerolog.Nop()) diff --git a/zetaclient/tss/testdata/pre-params.json b/zetaclient/tss/testdata/pre-params.json new file mode 100644 index 0000000000..7de86020b9 --- /dev/null +++ b/zetaclient/tss/testdata/pre-params.json @@ -0,0 +1,14 @@ +{ + "PaillierSK": { + "N": 19999163952130747960789760351783788437756051987951355569970036401244501934315272438705577988944467922102492970155408252851533852794097112218266886553586076347804115557757017279985449126837870194546917950194158856088817869376589018796114667864086028598566847973861695589083004303196605694312963960967331453606698750471421102641235873021677307589731091486775521460999458082171513731685071918681293270385071153104334613903503586831444874346531652054016464538970392294909014876824634633871501554184148693655829401315639013286485392467863637678737554558389111485917683916225530045382764297294039119124600051351150814211249, + "LambdaN": 9999581976065373980394880175891894218878025993975677784985018200622250967157636219352788994472233961051246485077704126425766926397048556109133443276793038173902057778878508639992724563418935097273458975097079428044408934688294509398057333932043014299283423986930847794541502151598302847156481980483665726803207844439805564908826908299383131049732286281017413122919529611290634931314211468373848862938241694978602167585393762136625122435580962508878486729843009777923588471302593033407043805894076580681612304239013813560948585228383852537633607722118782082256871296347729191161115127804109535019576521175406883452282, + "PhiN": 19999163952130747960789760351783788437756051987951355569970036401244501934315272438705577988944467922102492970155408252851533852794097112218266886553586076347804115557757017279985449126837870194546917950194158856088817869376589018796114667864086028598566847973861695589083004303196605694312963960967331453606415688879611129817653816598766262099464572562034826245839059222581269862628422936747697725876483389957204335170787524273250244871161925017756973459686019555847176942605186066814087611788153161363224608478027627121897170456767705075267215444237564164513742592695458382322230255608219070039153042350813766904564 + }, + "NTildei": 27751913785731317561401314588152253217728532341018033146570952349857948266439815804220817588815467446516307716665513549241775208783937988610417256936545867146994706743041371293345131680171448151086694170309671283486292778243426639412725044938992517873701725006330137070569134489394538234586796972674797327961558527707455425675762370744764931280145514019295136133284528084915871623701598565821330577968131284073671734400158797160800982150954634593880871530904229027402996176777363730066986245815341196905861357768072141609870245229405943812356754513322852513347434681734563805660510650984453048567481135942146732097813, + "H1i": 27561956258855841139395484606566450906572976054374340673395795572404700162978995778059515299485228139352425772962548084873215102616280128304708426026182349406021553685331715999823302504724574932475811252902805356309567749243825500782237349965272997278195732138043060594209278009387869413139123717258217760369558773026393837335151469655513584729163915760124835545159150578571574024242102852384898501031964398164508802427280019310461284676702762930983763592670681066746406519089087470866348555388978893174745329039290345402036056193885195412683078301439714912339014313158992229100758385774991450029591789628252080327559, + "H2i": 3728689158497086873141839685259803753136983319082693856963169726229733672131113136177490814524417255268612620467671366860707614533735757821359190975354246119660178366078082228715114521844446138293061922109534401626012196214235577165710689292018952534202208597250579673907469566293203565367500274532873330046549157395771416744602137701132458921345439713375591947533341059776041205066197520027367159385233699483111938755605800865028185017605864002287172205806900860500164647247828301974000635262077178937664668984154402040254899639546189675216600248280397892382489630326684820331235755629614367050680421011963919488203, + "Alpha": 12023422531214934687129519427336751009035984170561091401995316889932876861127740921311384022403289655727852687123651207179666808127441651093217171624401095992093222930319273470697036100721360499771918420525219595960686726656705243231703751035078246024613303037202379961560592763983837502239948740870420991159486603826605562036000339449263579950064527369636354957917745420111193837730149039024242588057472210146671375468285242828724250764829561800066210708917523476384078739610379308948169897901580561000198663019628193284829226489567395610248716149143318042255879731755585576865932443849122426445714440683236201534278, + "Beta": 5216977155731439778993461291447860640876449566364806008925260703987381460229691893122780873808995223223163744678138135687392695130955126431701144250850716493320323144762645954407840190267536639011747593827794567732053411766358287215370001772078774800656600220349293422391251849464836894526916992757090542649120892603149475768335549307299570724464502418555347605473575788163359985939167794314377636554352661162839551390523751777303683251484579852979700993668013952132183651009302732527581967714766865349243790338837470546064122938436752503268419712265039843732584942630907069295257952472051299690267622482814099402925, + "P": 88381187817350292364427405327147330703367287364982016684112982632296448146824918798880619726847439553397534386831375002209019191421648634648182506013318106307619159619988111117766095040541079223450318951393068166232842380382197780515682551401156068869281037660390394768227141311012971150741063274439667450459, + "Q": 78500624598652662628885184600358941803683076507070394222241292020467077683298789581328374906370256776628646773897006681776229150698724417192594456702517705509422164083826765704615434585654776671817770799114758241307176860136745886869834508666912875775642490370066102181451552981996440342363450244264823933513 +} \ No newline at end of file From 7d9c96b3904bb36d5ece495a6cb91e775c2cca95 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 22 Nov 2024 16:39:06 +0100 Subject: [PATCH 36/37] Fix TSS migration test --- contrib/localnet/orchestrator/start-zetae2e.sh | 3 ++- e2e/e2etests/test_migrate_tss.go | 3 ++- e2e/runner/bitcoin.go | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/contrib/localnet/orchestrator/start-zetae2e.sh b/contrib/localnet/orchestrator/start-zetae2e.sh index 99ab61d700..63064cdd1a 100644 --- a/contrib/localnet/orchestrator/start-zetae2e.sh +++ b/contrib/localnet/orchestrator/start-zetae2e.sh @@ -186,7 +186,8 @@ if [ "$LOCALNET_MODE" == "tss-migrate" ]; then echo "waiting 10 seconds for node to restart" sleep 10 - zetae2e local --skip-setup --config "$deployed_config_path" --skip-bitcoin-setup --light --skip-header-proof + zetae2e local --skip-setup --config "$deployed_config_path" \ + --skip-bitcoin-setup --light --skip-header-proof --skip-precompiles ZETAE2E_EXIT_CODE=$? if [ $ZETAE2E_EXIT_CODE -eq 0 ]; then echo "E2E passed after migration" diff --git a/e2e/e2etests/test_migrate_tss.go b/e2e/e2etests/test_migrate_tss.go index 067623a325..08ec888d71 100644 --- a/e2e/e2etests/test_migrate_tss.go +++ b/e2e/e2etests/test_migrate_tss.go @@ -20,10 +20,11 @@ import ( ) func TestMigrateTSS(r *runner.E2ERunner, _ []string) { + r.SetupBtcAddress(r.Name, false) stop := r.MineBlocksIfLocalBitcoin() defer stop() - // Pause inbound procoessing for tss migration + // Pause inbound processing for tss migration r.Logger.Info("Pause inbound processing") msg := observertypes.NewMsgDisableCCTX( r.ZetaTxServer.MustGetAccountAddressFromName(utils.EmergencyPolicyName), diff --git a/e2e/runner/bitcoin.go b/e2e/runner/bitcoin.go index bec7a1e907..ab322532a8 100644 --- a/e2e/runner/bitcoin.go +++ b/e2e/runner/bitcoin.go @@ -422,6 +422,8 @@ func (r *E2ERunner) QueryOutboundReceiverAndAmount(txid string) (string, int64) // and returns a channel that can be used to stop the mining // If the chain is not local, the function does nothing func (r *E2ERunner) MineBlocksIfLocalBitcoin() func() { + require.NotNil(r, r.BTCDeployerAddress, "E2ERunner.BTCDeployerAddress is nil") + stopChan := make(chan struct{}) go func() { for { From 2c02cc2812b77eb75c54c0e31143749d150554e1 Mon Sep 17 00:00:00 2001 From: Dmitry S <11892559+swift1337@users.noreply.github.com> Date: Fri, 22 Nov 2024 20:33:16 +0100 Subject: [PATCH 37/37] Fix btc issue --- e2e/e2etests/test_migrate_tss.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/e2etests/test_migrate_tss.go b/e2e/e2etests/test_migrate_tss.go index 08ec888d71..545e4abc55 100644 --- a/e2e/e2etests/test_migrate_tss.go +++ b/e2e/e2etests/test_migrate_tss.go @@ -20,7 +20,7 @@ import ( ) func TestMigrateTSS(r *runner.E2ERunner, _ []string) { - r.SetupBtcAddress(r.Name, false) + r.SetupBtcAddress(false) stop := r.MineBlocksIfLocalBitcoin() defer stop()