diff --git a/changelog.md b/changelog.md index 01b6f7bd66..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 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 0ac52f0690..76a3c0808c 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -2,50 +2,42 @@ package main import ( "context" - "encoding/json" - "fmt" - "io" "net/http" _ "net/http/pprof" // #nosec G108 -- pprof enablement is intentional "os" "os/signal" "path/filepath" "strings" - "sync" "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" "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/keys" "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" ) -// todo revamp -// https://github.com/zeta-chain/node/issues/3119 -// https://github.com/zeta-chain/node/issues/3112 -var preParams *ecdsakeygen.LocalPreParams +const ( + // enables posting blame data to core for failed TSS signatures + envFlagPostBlame = "POST_BLAME" + envPprofAddr = "PPROF_ADDR" +) +// Start starts zetaclientd process todo revamp +// https://github.com/zeta-chain/node/issues/3112 func Start(_ *cobra.Command, _ []string) error { // Prompt for Hotkey, TSS key-share and relayer key passwords titles := []string{"HotKey", "TSS", "Solana Relayer Key"} @@ -69,13 +61,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") - } - } - masterLogger := logger.Std startLogger := logger.Std.With().Str("module", "startup").Logger() @@ -95,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) @@ -150,189 +137,42 @@ 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]) - - // Generate pre Params if not present already - peers, err := initPeers(cfg.Peer) - if err != nil { - log.Error().Err(err).Msg("peer address error") - } - initPreParams(cfg.PreParamsPath) - 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) + telemetryServer.SetIPAddress(cfg.PublicIP) + + granteePubKeyBech32, err := resolveObserverPubKeyBech32(cfg, hotkeyPass) if err != nil { - startLogger.Error().Err(err).Msg("GetTssHistory error") + return errors.Wrap(err, "unable to resolve observer pub key bech32") } - 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, + GranteePubKeyBech32: granteePubKeyBech32, + HotKeyPassword: hotkeyPass, + TSSKeyPassword: tssKeyPass, + BitcoinChainIDs: btcChainIDsFromContext(appContext), + PostBlame: isEnvFlagEnabled(envFlagPostBlame), + Telemetry: telemetryServer, } - // Create TSS server - tssServer, err := mc.SetupTSSServer( - peers, - priKey, - preParams, - 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) - pingRTTLock := sync.Mutex{} - 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) - pingRTTLock.Lock() - defer pingRTTLock.Unlock() - 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) - } - }() - // 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") - } - }() - - // 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) - if err != nil { - return err - } - - tss, err := mc.New( - ctx, - zetacoreClient, - tssHistoricalList, - hotkeyPass, - tssServer, - ) - if err != nil { - startLogger.Error().Err(err).Msg("NewTSS 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) - } - } - - // 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 - } - // Starts various background TSS listeners. // Shuts down zetaclientd if any is triggered. maintenance.NewTSSListener(zetacoreClient, masterLogger).Listen(ctx, func() { @@ -423,42 +263,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) @@ -476,3 +280,30 @@ func isObserverNode(ctx context.Context, client *zetacore.Client) (bool, error) return false, nil } + +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 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 + err := http.ListenAndServe(addr, nil) + if err != nil { + logger.Error().Err(err).Msg("pprof http server error") + } +} 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/contrib/localnet/docker-compose.yml b/contrib/localnet/docker-compose.yml index 4e044fc0cf..2494cc0247 100644 --- a/contrib/localnet/docker-compose.yml +++ b/contrib/localnet/docker-compose.yml @@ -110,7 +110,10 @@ 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 volumes: - ssh:/root/.ssh - preparams:/root/preparams 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..545e4abc55 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(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 { 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/chains/base/observer.go b/zetaclient/chains/base/observer.go index 9056e3aa1d..5e7eebaf30 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 a339b051dd..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) } @@ -148,7 +147,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{} @@ -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..131fbe963f 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.BitcoinMainnet, + 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 d7632ac192..8b3a160450 100644 --- a/zetaclient/chains/evm/observer/inbound.go +++ b/zetaclient/chains/evm/observer/inbound.go @@ -614,7 +614,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 { @@ -806,7 +806,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..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) @@ -117,7 +115,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 +180,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 +198,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 +212,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/observer/outbound.go b/zetaclient/chains/evm/observer/outbound.go index f8ce8f32ba..2998f094dc 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 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: + 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 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 5cebb323c1..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) @@ -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/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..4165021f0e 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 @@ -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/chains/interfaces/interfaces.go b/zetaclient/chains/interfaces/interfaces.go index 4ed7c0797e..c13a194eb5 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 @@ -110,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) @@ -226,26 +233,7 @@ 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 + 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/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 020c781b3a..699253ff3d 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" @@ -53,10 +55,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 @@ -80,7 +85,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) @@ -154,7 +159,7 @@ func Test_CheckFinalizedTx(t *testing.T) { t.Run("should return error on ECDSA signer mismatch", func(t *testing.T) { // ARRANGE // create observer with other TSS address - tssOther := mocks.NewMockTSS(chain, sample.EthAddress().String(), "") + tssOther := mocks.NewTSS(t) ob := createTestObserver(t, chain, solClient, tssOther) // ACT 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 aaeae17f38..93303fb5e6 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/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 45a79f65ec..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)) @@ -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) @@ -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.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..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)) @@ -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/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/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/orchestrator/bootstap_test.go b/zetaclient/orchestrator/bootstap_test.go index c93efada00..d5f64c8500 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) @@ -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") 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 3fa003a3c0..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, _ string) ([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/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/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/config.go b/zetaclient/tss/config.go new file mode 100644 index 0000000000..06a24e87ed --- /dev/null +++ b/zetaclient/tss/config.go @@ -0,0 +1,123 @@ +package tss + +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" + + "github.com/zeta-chain/node/cmd" +) + +const ( + // Port is the default port for go-tss server. + Port = 6668 + Version = "0.14.0" + 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 == "" { + 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 +} + +// 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/config_test.go b/zetaclient/tss/config_test.go new file mode 100644 index 0000000000..1c220a70f6 --- /dev/null +++ b/zetaclient/tss/config_test.go @@ -0,0 +1,131 @@ +package tss + +import ( + _ "embed" + "fmt" + "os" + "path/filepath" + "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 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) + 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) + } +} + +//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.go b/zetaclient/tss/crypto.go new file mode 100644 index 0000000000..170f433657 --- /dev/null +++ b/zetaclient/tss/crypto.go @@ -0,0 +1,234 @@ +package tss + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "encoding/base64" + "encoding/hex" + "fmt" + "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" + "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 +} + +// 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, +// 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) + if err != nil { + return "" // not possible + } + + 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, 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(hash, actualMsgHash): + return [65]byte{}, errors.Errorf("message hash mismatch (got 0x%x, want 0x%x)", actualMsgHash, hash) + } + + 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(hash, sigBytes[:]) + switch { + 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.Errorf( + "public key mismatch (got %s, want %s)", + crypto.PubkeyToAddress(*actualPubKey), + pk.AddressEVM(), + ) + } + + 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 +} + +// 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.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]) + 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, "") + 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/crypto_test.go b/zetaclient/tss/crypto_test.go new file mode 100644 index 0000000000..b34e0dc21e --- /dev/null +++ b/zetaclient/tss/crypto_test.go @@ -0,0 +1,84 @@ +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/pkg/chains" +) + +func TestPubKey(t *testing.T) { + t.Run("Invalid", func(t *testing.T) { + 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 NewPubKeyFromBech32", 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()) + + // Check that NewPubKeyFromECDSA works + pk2, err := NewPubKeyFromECDSA(*pk.ecdsaPubKey) + 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/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/healthcheck.go b/zetaclient/tss/healthcheck.go new file mode 100644 index 0000000000..ea23b17de3 --- /dev/null +++ b/zetaclient/tss/healthcheck.go @@ -0,0 +1,121 @@ +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 _, peerID := range p.WhitelistPeers { + if peerID == host.ID() { + continue + } + + wg.Add(1) + + go func(peerID peer.ID) { + 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() + }(peerID) + } + + 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/keygen.go b/zetaclient/tss/keygen.go new file mode 100644 index 0000000000..dc2a120262 --- /dev/null +++ b/zetaclient/tss/keygen.go @@ -0,0 +1,326 @@ +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" + + "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" +) + +const ( + receiveSuccess = chains.ReceiveStatus_success + receiveFailed = chains.ReceiveStatus_failed +) + +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. +// 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: server, + 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 && !errors.Is(err, context.Canceled) { + 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 + } + } + + 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. +// - 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) { + k.iterations++ + + 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(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 + } + + 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 (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 { + return "", err + } + + hasher := sha3.NewLegacyKeccak256() + hasher.Write(bytes) + digest := hex.EncodeToString(hasher.Sum(nil)) + + return digest, nil +} + +var testKeySignData = []byte("hello meta") + +// TestKeySign performs a TSS key-sign test of sample data. +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(). + Str("keysign.test_data", string(testKeySignData)). + Str("keysign.test_data_hash", hashedData.String()). + Msg("Performing TSS key-sign test") + + req := keysign.NewRequest( + tssPubKey.Bech32String(), + []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/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 diff --git a/zetaclient/tss/service.go b/zetaclient/tss/service.go new file mode 100644 index 0000000000..7a8391ff89 --- /dev/null +++ b/zetaclient/tss/service.go @@ -0,0 +1,323 @@ +package tss + +import ( + "context" + "encoding/hex" + "fmt" + "time" + + "github.com/libp2p/go-libp2p/core/peer" + "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" + keyinterfaces "github.com/zeta-chain/node/zetaclient/keys/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) +} + +// 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) + SetConnectedPeers(peers []peer.AddrInfo) + SetPingRTT(peers map[peer.ID]int64) +} + +// Service TSS service +type Service struct { + zetacore Zetacore + tss KeySigner + currentPubKey PubKey + + postBlame bool + 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. +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 + } +} + +// WithMetrics registers Prometheus metrics for the TSS service. +// Otherwise, no metrics will be collected. +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 { + 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 LRU cache +func NewService( + keySigner KeySigner, + tssPubKeyBech32 string, + zetacore Zetacore, + logger zerolog.Logger, + opts ...Opt, +) (*Service, error) { + logger = logger.With().Str(logs.FieldModule, "tss_service").Logger() + + 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") + } + } + + // 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") + } + + return &Service{ + tss: keySigner, + currentPubKey: currentPubKey, + zetacore: zetacore, + postBlame: cfg.postBlame, + metrics: cfg.metrics, + 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] = base64EncodeString(digest) + } + + // #nosec G115 always in range + req := keysign.NewRequest( + s.PubKey().Bech32String(), + digestsBase64, + int64(height), + nil, + Version, + ) + + res, err := s.sign(req, nonce, chainID) + 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: signatures length mismatch (got %d, want %d)", + len(res.Signatures), + len(digests), + ) + } + + 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 sigs, nil +} + +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, 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) + + 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) + } + + 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) +} + +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") + + // register blame metrics + for _, node := range res.Blame.BlameNodes { + s.metrics.NodeBlamePerPubKey.WithLabelValues(node.Pubkey).Inc() + } + + 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 +} + +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..e0ccde6954 --- /dev/null +++ b/zetaclient/tss/service_test.go @@ -0,0 +1,238 @@ +package tss_test + +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/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) { + 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()) + 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 := tss.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 := tss.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) + }) + }) + + 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 { + *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 +} + +// 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") + } + + var ( + msgs = lo.Map(digests, func(digest []byte, _ int) string { + return base64EncodeString(digest) + }) + + req = keysign.NewRequest(pk, msgs, height, nil, tss.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: base64EncodeString(sig[:32]), + S: base64EncodeString(sig[32:64]), + RecoveryID: base64EncodeString(sig[64:65]), + }) + } + + // might be random... we should tolerate that + signatures = lo.Shuffle(signatures) + + 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) +} diff --git a/zetaclient/tss/setup.go b/zetaclient/tss/setup.go new file mode 100644 index 0000000000..9f9343beb7 --- /dev/null +++ b/zetaclient/tss/setup.go @@ -0,0 +1,331 @@ +package tss + +import ( + "context" + "fmt" + "os" + "path/filepath" + "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/logs" + "github.com/zeta-chain/node/zetaclient/metrics" +) + +// SetupProps represents options for Setup. +type SetupProps struct { + 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, +// 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())) + } + + 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. Bootstrap go-tss TSS server + tssServer, err := NewTSSServer( + bootstrapPeers, + whitelistedPeers, + tssPreParams, + hotPrivateKeyECDSA, + p.Config, + 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) + tssInfo, err := KeygenCeremony(ctx, tssServer, p.Zetacore, logger) + if err != nil { + return nil, errors.Wrap(err, "unable to perform keygen ceremony") + } + + historicalTSSInfo, err := p.Zetacore.GetTSSHistory(ctx) + if err != nil { + return nil, errors.Wrap(err, "unable to get TSS history") + } + + // 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") + } + + logger.Info().Msg("Key shared verified") + + // 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") + } + } + + // 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") + + // 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") + } + + logger.Info().Msg("TSS addresses validated. Starting healthcheck worker") + + healthCheckProps := HealthcheckProps{ + Telemetry: p.Telemetry, + WhitelistPeers: whitelistedPeers, + 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") + } + + 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") + } + + tssPath = filepath.Join(home, ".tss") + 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 + } + + wasPartOfTSS := func(grantees []string) bool { + return slices.Contains(grantees, p.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 +} 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 diff --git a/zetaclient/tss/tss_signer.go b/zetaclient/tss/tss_signer.go deleted file mode 100644 index bb887d9380..0000000000 --- a/zetaclient/tss/tss_signer.go +++ /dev/null @@ -1,706 +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, - 6668, - 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 -// 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) { - 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( - tssPubkey, - []string{base64.StdEncoding.EncodeToString(H)}, - 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().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") - } - - 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)) - if err != nil { - log.Error().Err(err).Msg("decoding signature RecoveryID") - return [65]byte{}, fmt.Errorf("signuature verification failue (V) %w", err) - } - - return sigbyte, 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 -} - -// 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, - "0.14.0", - ) - 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[:], "") - 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 -} 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) - } - }) - } -}