diff --git a/changelog.md b/changelog.md index af31bdc52d..f70614ef20 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,7 @@ * [2861](https://github.com/zeta-chain/node/pull/2861) - emit events from staking precompile * [2870](https://github.com/zeta-chain/node/pull/2870) - support for multiple Bitcoin chains in the zetaclient * [2883](https://github.com/zeta-chain/node/pull/2883) - add chain static information for btc signet testnet +* [2907](https://github.com/zeta-chain/node/pull/2907) - derive Bitcoin tss address by chain id and added more Signet static info * [2904](https://github.com/zeta-chain/node/pull/2904) - integrate authenticated calls smart contract functionality into protocol ### Refactor diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index c46cfc4f3a..f00abdcde1 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -13,7 +13,6 @@ import ( "time" "github.com/cometbft/cometbft/crypto/secp256k1" - ethcommon "github.com/ethereum/go-ethereum/common" maddr "github.com/multiformats/go-multiaddr" "github.com/pkg/errors" "github.com/rs/zerolog/log" @@ -230,21 +229,10 @@ func start(_ *cobra.Command, _ []string) error { return err } - btcChains := appContext.FilterChains(zctx.Chain.IsBitcoin) - switch { - case len(btcChains) == 0: - return errors.New("no BTC chains found") - case len(btcChains) > 1: - // In the future we might support multiple UTXO chains; - // right now we only support BTC. Let's make sure there are no surprises. - return errors.New("more than one BTC chain found") - } - tss, err := mc.NewTSS( ctx, zetacoreClient, tssHistoricalList, - btcChains[0].ID(), hotkeyPass, server, ) @@ -280,16 +268,20 @@ func start(_ *cobra.Command, _ []string) error { return err } - // Defensive check: Make sure the tss address is set to the current TSS address and not the newly generated one + // 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 - if tss.EVMAddress() == (ethcommon.Address{}) || tss.BTCAddress() == "" { - startLogger.Error().Msg("TSS address is not set in zetacore") - } else { - startLogger.Info(). - Str("tss.eth", tss.EVMAddress().String()). - Str("tss.btc", tss.BTCAddress()). - Str("tss.pub_key", tss.CurrentPubkey). - Msg("Current TSS") + err = tss.ValidateAddresses(btcChainIDs) + if err != nil { + startLogger.Error().Err(err).Msg("TSS address validation failed") + return err } if len(appContext.ListChainIDs()) == 0 { diff --git a/e2e/e2etests/test_bitcoin_deposit_call.go b/e2e/e2etests/test_bitcoin_deposit_call.go index 0f1fd6dc91..c79ca9c1b8 100644 --- a/e2e/e2etests/test_bitcoin_deposit_call.go +++ b/e2e/e2etests/test_bitcoin_deposit_call.go @@ -34,7 +34,7 @@ func TestBitcoinDepositAndCall(r *runner.E2ERunner, args []string) { // deploy an example contract in ZEVM contractAddr, _, contract, err := testcontract.DeployExample(r.ZEVMAuth, r.ZEVMClient) require.NoError(r, err) - r.Logger.Print("Bitcoin: Example contract deployed at: %s", contractAddr.String()) + r.Logger.Info("Bitcoin: Example contract deployed at: %s", contractAddr.String()) // ACT // Send BTC to TSS address with a dummy memo diff --git a/e2e/runner/bitcoin.go b/e2e/runner/bitcoin.go index 60c6ae3025..6fbd6b40d7 100644 --- a/e2e/runner/bitcoin.go +++ b/e2e/runner/bitcoin.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" @@ -296,7 +297,7 @@ func (r *E2ERunner) GetBitcoinChainID() int64 { // IsLocalBitcoin returns true if the runner is running on a local bitcoin network func (r *E2ERunner) IsLocalBitcoin() bool { - return r.BitcoinParams.Name == chains.BitcoinRegnetParams.Name + return r.BitcoinParams.Name == chaincfg.RegressionNetParams.Name } // GenerateToAddressIfLocalBitcoin generates blocks to an address if the runner is interacting diff --git a/pkg/chains/bitcoin.go b/pkg/chains/bitcoin.go index e145ef7668..211722cf9c 100644 --- a/pkg/chains/bitcoin.go +++ b/pkg/chains/bitcoin.go @@ -7,37 +7,37 @@ import ( ) var ( - BitcoinMainnetParams = &chaincfg.MainNetParams - BitcoinRegnetParams = &chaincfg.RegressionNetParams - BitcoinTestnetParams = &chaincfg.TestNet3Params + // chainIDToNetworkParams maps the Bitcoin chain ID to the network parameters + chainIDToNetworkParams = map[int64]*chaincfg.Params{ + BitcoinRegtest.ChainId: &chaincfg.RegressionNetParams, + BitcoinMainnet.ChainId: &chaincfg.MainNetParams, + BitcoinTestnet.ChainId: &chaincfg.TestNet3Params, + BitcoinSignetTestnet.ChainId: &chaincfg.SigNetParams, + } + + // networkNameToChainID maps the Bitcoin network name to the chain ID + networkNameToChainID = map[string]int64{ + chaincfg.RegressionNetParams.Name: BitcoinRegtest.ChainId, + chaincfg.MainNetParams.Name: BitcoinMainnet.ChainId, + chaincfg.TestNet3Params.Name: BitcoinTestnet.ChainId, + chaincfg.SigNetParams.Name: BitcoinSignetTestnet.ChainId, + } ) // BitcoinNetParamsFromChainID returns the bitcoin net params to be used from the chain id func BitcoinNetParamsFromChainID(chainID int64) (*chaincfg.Params, error) { - switch chainID { - case BitcoinRegtest.ChainId: - return BitcoinRegnetParams, nil - case BitcoinMainnet.ChainId: - return BitcoinMainnetParams, nil - case BitcoinTestnet.ChainId: - return BitcoinTestnetParams, nil - default: - return nil, fmt.Errorf("no Bitcoin net params for chain ID: %d", chainID) + if params, found := chainIDToNetworkParams[chainID]; found { + return params, nil } + return nil, fmt.Errorf("no Bitcoin network params for chain ID: %d", chainID) } // BitcoinChainIDFromNetworkName returns the chain id for the given bitcoin network name func BitcoinChainIDFromNetworkName(name string) (int64, error) { - switch name { - case BitcoinRegnetParams.Name: - return BitcoinRegtest.ChainId, nil - case BitcoinMainnetParams.Name: - return BitcoinMainnet.ChainId, nil - case BitcoinTestnetParams.Name: - return BitcoinTestnet.ChainId, nil - default: - return 0, fmt.Errorf("invalid Bitcoin network name: %s", name) + if chainID, found := networkNameToChainID[name]; found { + return chainID, nil } + return 0, fmt.Errorf("invalid Bitcoin network name: %s", name) } // IsBitcoinRegnet returns true if the chain id is for the regnet diff --git a/pkg/chains/bitcoin_test.go b/pkg/chains/bitcoin_test.go index 592e6ad424..ab344c7f95 100644 --- a/pkg/chains/bitcoin_test.go +++ b/pkg/chains/bitcoin_test.go @@ -14,9 +14,10 @@ func TestBitcoinNetParamsFromChainID(t *testing.T) { expected *chaincfg.Params wantErr bool }{ - {"Regnet", BitcoinRegtest.ChainId, BitcoinRegnetParams, false}, - {"Mainnet", BitcoinMainnet.ChainId, BitcoinMainnetParams, false}, - {"Testnet", BitcoinTestnet.ChainId, BitcoinTestnetParams, false}, + {"Regnet", BitcoinRegtest.ChainId, &chaincfg.RegressionNetParams, false}, + {"Mainnet", BitcoinMainnet.ChainId, &chaincfg.MainNetParams, false}, + {"Testnet", BitcoinTestnet.ChainId, &chaincfg.TestNet3Params, false}, + {"Signet", BitcoinSignetTestnet.ChainId, &chaincfg.SigNetParams, false}, {"Unknown", -1, nil, true}, } @@ -25,9 +26,10 @@ func TestBitcoinNetParamsFromChainID(t *testing.T) { params, err := BitcoinNetParamsFromChainID(tt.chainID) if tt.wantErr { require.Error(t, err) + require.Nil(t, params) } else { require.NoError(t, err) - require.Equal(t, tt.expected, params) + require.EqualValues(t, tt.expected, params) } }) } @@ -40,9 +42,10 @@ func TestBitcoinChainIDFromNetParams(t *testing.T) { expectedChainID int64 wantErr bool }{ - {"Regnet", BitcoinRegnetParams.Name, BitcoinRegtest.ChainId, false}, - {"Mainnet", BitcoinMainnetParams.Name, BitcoinMainnet.ChainId, false}, - {"Testnet", BitcoinTestnetParams.Name, BitcoinTestnet.ChainId, false}, + {"Regnet", chaincfg.RegressionNetParams.Name, BitcoinRegtest.ChainId, false}, + {"Mainnet", chaincfg.MainNetParams.Name, BitcoinMainnet.ChainId, false}, + {"Testnet", chaincfg.TestNet3Params.Name, BitcoinTestnet.ChainId, false}, + {"Signet", chaincfg.SigNetParams.Name, BitcoinSignetTestnet.ChainId, false}, {"Unknown", "Unknown", 0, true}, } diff --git a/x/crosschain/keeper/cctx_orchestrator_validate_inbound_test.go b/x/crosschain/keeper/cctx_orchestrator_validate_inbound_test.go index 1a65f2ee8d..03e8d607db 100644 --- a/x/crosschain/keeper/cctx_orchestrator_validate_inbound_test.go +++ b/x/crosschain/keeper/cctx_orchestrator_validate_inbound_test.go @@ -577,7 +577,7 @@ func TestKeeper_CheckMigration(t *testing.T) { } err := k.CheckIfTSSMigrationTransfer(ctx, &msg) - require.ErrorContains(t, err, "no Bitcoin net params for chain ID: 999") + require.ErrorContains(t, err, "no Bitcoin network params for chain ID: 999") }) t.Run("fails if gateway is not observer ", func(t *testing.T) { diff --git a/x/observer/keeper/grpc_query_tss.go b/x/observer/keeper/grpc_query_tss.go index 5ad108d06e..6874c6bade 100644 --- a/x/observer/keeper/grpc_query_tss.go +++ b/x/observer/keeper/grpc_query_tss.go @@ -4,6 +4,7 @@ import ( "context" "sort" + "github.com/btcsuite/btcd/chaincfg" sdk "github.com/cosmos/cosmos-sdk/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -54,7 +55,7 @@ func (k Keeper) GetTssAddress( if err != nil { return nil, status.Error(codes.Internal, err.Error()) } - bitcoinParams := chains.BitcoinRegnetParams + bitcoinParams := &chaincfg.RegressionNetParams if req.BitcoinChainId != 0 { bitcoinParams, err = chains.BitcoinNetParamsFromChainID(req.BitcoinChainId) if err != nil { @@ -88,7 +89,7 @@ func (k Keeper) GetTssAddressByFinalizedHeight( if err != nil { return nil, status.Error(codes.Internal, err.Error()) } - bitcoinParams := chains.BitcoinRegnetParams + bitcoinParams := &chaincfg.RegressionNetParams if req.BitcoinChainId != 0 { bitcoinParams, err = chains.BitcoinNetParamsFromChainID(req.BitcoinChainId) if err != nil { diff --git a/zetaclient/chains/base/observer.go b/zetaclient/chains/base/observer.go index 5ad8a6990b..6cf8af9de6 100644 --- a/zetaclient/chains/base/observer.go +++ b/zetaclient/chains/base/observer.go @@ -217,6 +217,22 @@ func (ob *Observer) WithTSS(tss interfaces.TSSSigner) *Observer { return ob } +// TSSAddressString returns the TSS address for the chain. +// +// Note: all chains uses TSS EVM address except Bitcoin chain. +func (ob *Observer) TSSAddressString() string { + switch ob.chain.Consensus { + case chains.Consensus_bitcoin: + address, err := ob.tss.BTCAddress(ob.Chain().ChainId) + if err != nil { + return "" + } + return address.EncodeAddress() + default: + return ob.tss.EVMAddress().String() + } +} + // LastBlock get external last block height. func (ob *Observer) LastBlock() uint64 { return atomic.LoadUint64(&ob.lastBlock) @@ -286,11 +302,7 @@ func (ob *Observer) WithHeaderCache(cache *lru.Cache) *Observer { // OutboundID returns a unique identifier for the outbound transaction. // The identifier is now used as the key for maps that store outbound related data (e.g. transaction, receipt, etc). func (ob *Observer) OutboundID(nonce uint64) string { - // all chains uses EVM address as part of the key except bitcoin - tssAddress := ob.tss.EVMAddress().String() - if ob.chain.Consensus == chains.Consensus_bitcoin { - tssAddress = ob.tss.BTCAddress() - } + tssAddress := ob.TSSAddressString() return fmt.Sprintf("%d-%s-%d", ob.chain.ChainId, tssAddress, nonce) } diff --git a/zetaclient/chains/base/observer_test.go b/zetaclient/chains/base/observer_test.go index 1bbd1b00b5..0e772e31f9 100644 --- a/zetaclient/chains/base/observer_test.go +++ b/zetaclient/chains/base/observer_test.go @@ -7,10 +7,12 @@ import ( "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" @@ -21,6 +23,7 @@ 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" ) @@ -271,6 +274,60 @@ 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 + forceError bool + addrExpected string + }{ + { + name: "should return TSS BTC address for Bitcoin chain", + chain: chains.BitcoinMainnet, + addrExpected: testutils.TSSAddressBTCMainnet, + }, + { + name: "should return TSS EVM address for EVM chain", + chain: chains.Ethereum, + addrExpected: testutils.TSSAddressEVMMainnet, + }, + { + name: "should return TSS EVM address for other non-BTC chain", + chain: chains.SolanaDevnet, + addrExpected: testutils.TSSAddressEVMMainnet, + }, + { + name: "should return empty address for unknown BTC chain", + chain: chains.BitcoinMainnet, + forceError: true, + addrExpected: "", + }, + } + + // run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // create observer + ob := createObserver(t, tt.chain, defaultAlertLatency) + + // force error if needed + if tt.forceError { + // pause TSS to cause error + tss := mocks.NewTSSMainnet() + tss.Pause() + ob = ob.WithTSS(tss) + } + + // get TSS address + addr := ob.TSSAddressString() + require.Equal(t, tt.addrExpected, addr) + }) + } +} + func TestIsBlockConfirmed(t *testing.T) { tests := []struct { name string @@ -348,10 +405,7 @@ func TestOutboundID(t *testing.T) { outboundID := ob.OutboundID(tt.nonce) // expected outbound id - exepctedID := fmt.Sprintf("%d-%s-%d", tt.chain.ChainId, tt.tss.EVMAddress(), tt.nonce) - if tt.chain.Consensus == chains.Consensus_bitcoin { - exepctedID = fmt.Sprintf("%d-%s-%d", tt.chain.ChainId, tt.tss.BTCAddress(), tt.nonce) - } + exepctedID := fmt.Sprintf("%d-%s-%d", tt.chain.ChainId, ob.TSSAddressString(), tt.nonce) require.Equal(t, exepctedID, outboundID) }) } diff --git a/zetaclient/chains/bitcoin/observer/inbound.go b/zetaclient/chains/bitcoin/observer/inbound.go index c65626da23..e75d9cc1a0 100644 --- a/zetaclient/chains/bitcoin/observer/inbound.go +++ b/zetaclient/chains/bitcoin/observer/inbound.go @@ -146,7 +146,7 @@ func (ob *Observer) ObserveInbound(ctx context.Context) error { // add block header to zetacore if len(res.Block.Tx) > 1 { // filter incoming txs to TSS address - tssAddress := ob.TSS().BTCAddress() + tssAddress := ob.TSSAddressString() // #nosec G115 always positive events, err := FilterAndParseIncomingTx( diff --git a/zetaclient/chains/bitcoin/observer/observer.go b/zetaclient/chains/bitcoin/observer/observer.go index a699767b4c..80781fdd33 100644 --- a/zetaclient/chains/bitcoin/observer/observer.go +++ b/zetaclient/chains/bitcoin/observer/observer.go @@ -369,12 +369,11 @@ func (ob *Observer) FetchUTXOs(ctx context.Context) error { maxConfirmations := int(bh) // List all unspent UTXOs (160ms) - tssAddr := ob.TSS().BTCAddress() - address, err := chains.DecodeBtcAddress(tssAddr, ob.Chain().ChainId) + tssAddr, err := ob.TSS().BTCAddress(ob.Chain().ChainId) if err != nil { - return fmt.Errorf("btc: error decoding wallet address (%s) : %s", tssAddr, err.Error()) + return fmt.Errorf("error getting bitcoin tss address") } - utxos, err := ob.btcClient.ListUnspentMinMaxAddresses(0, maxConfirmations, []btcutil.Address{address}) + utxos, err := ob.btcClient.ListUnspentMinMaxAddresses(0, maxConfirmations, []btcutil.Address{tssAddr}) if err != nil { return err } diff --git a/zetaclient/chains/bitcoin/observer/outbound.go b/zetaclient/chains/bitcoin/observer/outbound.go index 01b1d16609..3b25d8c0c1 100644 --- a/zetaclient/chains/bitcoin/observer/outbound.go +++ b/zetaclient/chains/bitcoin/observer/outbound.go @@ -414,7 +414,7 @@ func (ob *Observer) getOutboundIDByNonce(ctx context.Context, nonce uint64, test // findNonceMarkUTXO finds the nonce-mark UTXO in the list of UTXOs. func (ob *Observer) findNonceMarkUTXO(nonce uint64, txid string) (int, error) { - tssAddress := ob.TSS().BTCAddressWitnessPubkeyHash().EncodeAddress() + tssAddress := ob.TSSAddressString() amount := chains.NonceMarkAmount(nonce) for i, utxo := range ob.utxos { sats, err := bitcoin.GetSatoshis(utxo.Amount) @@ -599,7 +599,7 @@ func (ob *Observer) checkTSSVout(params *crosschaintypes.OutboundParams, vouts [ } nonce := params.TssNonce - tssAddress := ob.TSS().BTCAddress() + tssAddress := ob.TSSAddressString() for _, vout := range vouts { // decode receiver and amount from vout receiverExpected := tssAddress @@ -658,7 +658,7 @@ func (ob *Observer) checkTSSVoutCancelled(params *crosschaintypes.OutboundParams } nonce := params.TssNonce - tssAddress := ob.TSS().BTCAddress() + tssAddress := ob.TSSAddressString() for _, vout := range vouts { // decode receiver and amount from vout receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, tssAddress, ob.Chain()) diff --git a/zetaclient/chains/bitcoin/observer/outbound_test.go b/zetaclient/chains/bitcoin/observer/outbound_test.go index ac9fb2609a..1507c7d416 100644 --- a/zetaclient/chains/bitcoin/observer/outbound_test.go +++ b/zetaclient/chains/bitcoin/observer/outbound_test.go @@ -61,27 +61,29 @@ func createObserverWithPrivateKey(t *testing.T) *Observer { func createObserverWithUTXOs(t *testing.T) *Observer { // Create Bitcoin observer ob := createObserverWithPrivateKey(t) - tssAddress := ob.TSS().BTCAddressWitnessPubkeyHash().EncodeAddress() + tssAddress, err := ob.TSS().BTCAddress(chains.BitcoinTestnet.ChainId) + require.NoError(t, err) // Create 10 dummy UTXOs (22.44 BTC in total) ob.utxos = make([]btcjson.ListUnspentResult, 0, 10) amounts := []float64{0.01, 0.12, 0.18, 0.24, 0.5, 1.26, 2.97, 3.28, 5.16, 8.72} for _, amount := range amounts { - ob.utxos = append(ob.utxos, btcjson.ListUnspentResult{Address: tssAddress, Amount: amount}) + ob.utxos = append(ob.utxos, btcjson.ListUnspentResult{Address: tssAddress.EncodeAddress(), Amount: amount}) } return ob } -func mineTxNSetNonceMark(ob *Observer, nonce uint64, txid string, preMarkIndex int) { +func mineTxNSetNonceMark(t *testing.T, ob *Observer, nonce uint64, txid string, preMarkIndex int) { // Mine transaction outboundID := ob.OutboundID(nonce) ob.includedTxResults[outboundID] = &btcjson.GetTransactionResult{TxID: txid} // Set nonce mark - tssAddress := ob.TSS().BTCAddressWitnessPubkeyHash().EncodeAddress() + tssAddress, err := ob.TSS().BTCAddress(chains.BitcoinTestnet.ChainId) + require.NoError(t, err) nonceMark := btcjson.ListUnspentResult{ TxID: txid, - Address: tssAddress, + Address: tssAddress.EncodeAddress(), Amount: float64(chains.NonceMarkAmount(nonce)) * 1e-8, } if preMarkIndex >= 0 { // replace nonce-mark utxo @@ -268,7 +270,7 @@ func TestSelectUTXOs(t *testing.T) { require.Nil(t, result) require.Zero(t, amount) require.Equal(t, "getOutboundIDByNonce: cannot find outbound txid for nonce 0", err.Error()) - mineTxNSetNonceMark(ob, 0, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 0 + mineTxNSetNonceMark(t, ob, 0, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 0 // Case3: nonce = 1, should pass now // input: utxoCap = 5, amount = 0.5, nonce = 1 @@ -277,7 +279,7 @@ func TestSelectUTXOs(t *testing.T) { require.NoError(t, err) require.Equal(t, 0.55002, amount) require.Equal(t, ob.utxos[0:5], result) - mineTxNSetNonceMark(ob, 1, dummyTxID, 0) // mine a transaction and set nonce-mark utxo for nonce 1 + mineTxNSetNonceMark(t, ob, 1, dummyTxID, 0) // mine a transaction and set nonce-mark utxo for nonce 1 // Case4: // input: utxoCap = 5, amount = 1.0, nonce = 2 @@ -286,7 +288,7 @@ func TestSelectUTXOs(t *testing.T) { require.NoError(t, err) require.InEpsilon(t, 1.05002001, amount, 1e-8) require.Equal(t, ob.utxos[0:6], result) - mineTxNSetNonceMark(ob, 2, dummyTxID, 0) // mine a transaction and set nonce-mark utxo for nonce 2 + mineTxNSetNonceMark(t, ob, 2, dummyTxID, 0) // mine a transaction and set nonce-mark utxo for nonce 2 // Case5: should include nonce-mark utxo on the LEFT // input: utxoCap = 5, amount = 8.05, nonce = 3 @@ -296,7 +298,7 @@ func TestSelectUTXOs(t *testing.T) { require.InEpsilon(t, 8.25002002, amount, 1e-8) expected := append([]btcjson.ListUnspentResult{ob.utxos[0]}, ob.utxos[4:9]...) require.Equal(t, expected, result) - mineTxNSetNonceMark(ob, 24105431, dummyTxID, 0) // mine a transaction and set nonce-mark utxo for nonce 24105431 + mineTxNSetNonceMark(t, ob, 24105431, dummyTxID, 0) // mine a transaction and set nonce-mark utxo for nonce 24105431 // Case6: should include nonce-mark utxo on the RIGHT // input: utxoCap = 5, amount = 0.503, nonce = 24105432 @@ -306,7 +308,7 @@ func TestSelectUTXOs(t *testing.T) { require.InEpsilon(t, 0.79107431, amount, 1e-8) expected = append([]btcjson.ListUnspentResult{ob.utxos[4]}, ob.utxos[0:4]...) require.Equal(t, expected, result) - mineTxNSetNonceMark(ob, 24105432, dummyTxID, 4) // mine a transaction and set nonce-mark utxo for nonce 24105432 + mineTxNSetNonceMark(t, ob, 24105432, dummyTxID, 4) // mine a transaction and set nonce-mark utxo for nonce 24105432 // Case7: should include nonce-mark utxo in the MIDDLE // input: utxoCap = 5, amount = 1.0, nonce = 24105433 @@ -348,7 +350,7 @@ func TestUTXOConsolidation(t *testing.T) { t.Run("should not consolidate", func(t *testing.T) { ob := createObserverWithUTXOs(t) - mineTxNSetNonceMark(ob, 0, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 0 + mineTxNSetNonceMark(t, ob, 0, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 0 // input: utxoCap = 10, amount = 0.01, nonce = 1, rank = 10 // output: [0.00002, 0.01], 0.01002 @@ -362,7 +364,7 @@ func TestUTXOConsolidation(t *testing.T) { t.Run("should consolidate 1 utxo", func(t *testing.T) { ob := createObserverWithUTXOs(t) - mineTxNSetNonceMark(ob, 0, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 0 + mineTxNSetNonceMark(t, ob, 0, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 0 // input: utxoCap = 9, amount = 0.01, nonce = 1, rank = 9 // output: [0.00002, 0.01, 0.12], 0.13002 @@ -376,7 +378,7 @@ func TestUTXOConsolidation(t *testing.T) { t.Run("should consolidate 3 utxos", func(t *testing.T) { ob := createObserverWithUTXOs(t) - mineTxNSetNonceMark(ob, 0, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 0 + mineTxNSetNonceMark(t, ob, 0, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 0 // input: utxoCap = 5, amount = 0.01, nonce = 0, rank = 5 // output: [0.00002, 0.014, 1.26, 0.5, 0.2], 2.01002 @@ -395,7 +397,7 @@ func TestUTXOConsolidation(t *testing.T) { t.Run("should consolidate all utxos using rank 1", func(t *testing.T) { ob := createObserverWithUTXOs(t) - mineTxNSetNonceMark(ob, 0, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 0 + mineTxNSetNonceMark(t, ob, 0, dummyTxID, -1) // mine a transaction and set nonce-mark utxo for nonce 0 // input: utxoCap = 12, amount = 0.01, nonce = 0, rank = 1 // output: [0.00002, 0.01, 8.72, 5.16, 3.28, 2.97, 1.26, 0.5, 0.24, 0.18, 0.12], 22.44002 @@ -415,6 +417,7 @@ func TestUTXOConsolidation(t *testing.T) { t.Run("should consolidate 3 utxos sparse", func(t *testing.T) { ob := createObserverWithUTXOs(t) mineTxNSetNonceMark( + t, ob, 24105431, dummyTxID, @@ -438,6 +441,7 @@ func TestUTXOConsolidation(t *testing.T) { t.Run("should consolidate all utxos sparse", func(t *testing.T) { ob := createObserverWithUTXOs(t) mineTxNSetNonceMark( + t, ob, 24105431, dummyTxID, diff --git a/zetaclient/chains/bitcoin/observer/rpc_status.go b/zetaclient/chains/bitcoin/observer/rpc_status.go index 008d49aa59..e0fc3c651d 100644 --- a/zetaclient/chains/bitcoin/observer/rpc_status.go +++ b/zetaclient/chains/bitcoin/observer/rpc_status.go @@ -29,7 +29,12 @@ func (ob *Observer) watchRPCStatus(_ context.Context) error { // checkRPCStatus checks the RPC status of the Bitcoin chain func (ob *Observer) checkRPCStatus() { - tssAddress := ob.TSS().BTCAddressWitnessPubkeyHash() + tssAddress, err := ob.TSS().BTCAddress(ob.Chain().ChainId) + if err != nil { + ob.Logger().Chain.Error().Err(err).Msg("unable to get TSS BTC address") + return + } + blockTime, err := rpc.CheckRPCStatus(ob.btcClient, tssAddress) if err != nil { ob.Logger().Chain.Error().Err(err).Msg("CheckRPCStatus failed") diff --git a/zetaclient/chains/bitcoin/signer/signer.go b/zetaclient/chains/bitcoin/signer/signer.go index b3e307713e..7e49d4d675 100644 --- a/zetaclient/chains/bitcoin/signer/signer.go +++ b/zetaclient/chains/bitcoin/signer/signer.go @@ -154,7 +154,10 @@ func (signer *Signer) AddWithdrawTxOutputs( } // 1st output: the nonce-mark btc to TSS self - tssAddrP2WPKH := signer.TSS().BTCAddressWitnessPubkeyHash() + tssAddrP2WPKH, err := signer.TSS().BTCAddress(signer.Chain().ChainId) + if err != nil { + return err + } payToSelfScript, err := bitcoin.PayToAddrScript(tssAddrP2WPKH) if err != nil { return err diff --git a/zetaclient/chains/bitcoin/signer/signer_keysign_test.go b/zetaclient/chains/bitcoin/signer/signer_keysign_test.go index 2bc40c366c..1ad50f0af5 100644 --- a/zetaclient/chains/bitcoin/signer/signer_keysign_test.go +++ b/zetaclient/chains/bitcoin/signer/signer_keysign_test.go @@ -16,6 +16,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/stretchr/testify/suite" + "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/zetaclient/chains/bitcoin" "github.com/zeta-chain/node/zetaclient/chains/interfaces" "github.com/zeta-chain/node/zetaclient/testutils/mocks" @@ -38,7 +39,8 @@ func (suite *BTCSignTestSuite) SetupTest() { suite.testSigner = &mocks.TSS{ // fake TSS PrivKey: privateKey.ToECDSA(), } - addr := suite.testSigner.BTCAddressWitnessPubkeyHash() + addr, err := suite.testSigner.BTCAddress(chains.BitcoinTestnet.ChainId) + suite.Require().NoError(err) suite.T().Logf("segwit addr: %s", addr) } diff --git a/zetaclient/chains/bitcoin/signer/signer_test.go b/zetaclient/chains/bitcoin/signer/signer_test.go index bd42ce283c..b9decf25b1 100644 --- a/zetaclient/chains/bitcoin/signer/signer_test.go +++ b/zetaclient/chains/bitcoin/signer/signer_test.go @@ -238,7 +238,8 @@ func TestAddWithdrawTxOutputs(t *testing.T) { require.NoError(t, err) // tss address and script - tssAddr := signer.TSS().BTCAddressWitnessPubkeyHash() + tssAddr, err := signer.TSS().BTCAddress(chains.BitcoinTestnet.ChainId) + require.NoError(t, err) tssScript, err := bitcoin.PayToAddrScript(tssAddr) require.NoError(t, err) fmt.Printf("tss address: %s", tssAddr.EncodeAddress()) diff --git a/zetaclient/chains/interfaces/interfaces.go b/zetaclient/chains/interfaces/interfaces.go index f5b6cfaf62..ab58456de6 100644 --- a/zetaclient/chains/interfaces/interfaces.go +++ b/zetaclient/chains/interfaces/interfaces.go @@ -255,9 +255,7 @@ type TSSSigner interface { SignBatch(ctx context.Context, digests [][]byte, height uint64, nonce uint64, chainID int64) ([][65]byte, error) EVMAddress() ethcommon.Address - EVMAddressList() []ethcommon.Address - BTCAddress() string - BTCAddressWitnessPubkeyHash() *btcutil.AddressWitnessPubKeyHash + BTCAddress(chainID int64) (*btcutil.AddressWitnessPubKeyHash, error) PubKeyCompressedBytes() []byte } diff --git a/zetaclient/testutils/constant.go b/zetaclient/testutils/constant.go index e9874a75b5..e9b8b53563 100644 --- a/zetaclient/testutils/constant.go +++ b/zetaclient/testutils/constant.go @@ -17,6 +17,9 @@ const ( // TSSAddressBTCMainnet the BTC TSS address for test purposes TSSAddressBTCMainnet = "bc1qm24wp577nk8aacckv8np465z3dvmu7ry45el6y" + // TSSPubkeyAthens3 is the TSS public key in Athens3 + TSSPubkeyAthens3 = "zetapub1addwnpepq28c57cvcs0a2htsem5zxr6qnlvq9mzhmm76z3jncsnzz32rclangr2g35p" + // TSSAddressEVMAthens3 the EVM TSS address for test purposes // Note: public key is zetapub1addwnpepq28c57cvcs0a2htsem5zxr6qnlvq9mzhmm76z3jncsnzz32rclangr2g35p TSSAddressEVMAthens3 = "0x8531a5aB847ff5B22D855633C25ED1DA3255247e" diff --git a/zetaclient/testutils/mocks/tss_signer.go b/zetaclient/testutils/mocks/tss_signer.go index b8d86f4f57..e0d9bd384c 100644 --- a/zetaclient/testutils/mocks/tss_signer.go +++ b/zetaclient/testutils/mocks/tss_signer.go @@ -112,27 +112,31 @@ func (s *TSS) EVMAddressList() []ethcommon.Address { return []ethcommon.Address{s.EVMAddress()} } -func (s *TSS) BTCAddress() string { - // force use btcAddress if set - if s.btcAddress != "" { - return s.btcAddress +func (s *TSS) BTCAddress(_ int64) (*btcutil.AddressWitnessPubKeyHash, error) { + // return error if tss is paused + if s.paused { + return nil, fmt.Errorf("tss is paused") } - testnet3Addr := s.btcAddressPubkey() - if testnet3Addr == nil { - return "" + // 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 } - return testnet3Addr.EncodeAddress() -} - -func (s *TSS) BTCAddressWitnessPubkeyHash() *btcutil.AddressWitnessPubKeyHash { // 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 + return nil, err } // witness program: https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#Witness_program @@ -143,23 +147,12 @@ func (s *TSS) BTCAddressWitnessPubkeyHash() *btcutil.AddressWitnessPubKeyHash { ) if err != nil { fmt.Printf("error NewAddressWitnessPubKeyHash: %v", err) - return nil + return nil, err } - return addrWPKH - } - - net, err := chains.GetBTCChainParams(s.chain.ChainId) - if err != nil { - fmt.Printf("error getting btc chain params: %v", err) - return nil - } - tssAddress := s.BTCAddress() - addr, err := btcutil.DecodeAddress(tssAddress, net) - if err != nil { - return nil + return addrWPKH, nil } - return addr.(*btcutil.AddressWitnessPubKeyHash) + return nil, nil } // PubKeyCompressedBytes returns 33B compressed pubkey @@ -173,19 +166,6 @@ func (s *TSS) PubKeyCompressedBytes() []byte { return pk.SerializeCompressed() } -func (s *TSS) btcAddressPubkey() *btcutil.AddressPubKey { - pkBytes := crypto.FromECDSAPub(&s.PrivKey.PublicKey) - pk, err := btcec.ParsePubKey(pkBytes) - if err != nil { - return nil - } - testnet3Addr, err := btcutil.NewAddressPubKey(pk.SerializeCompressed(), &chaincfg.TestNet3Params) - if err != nil { - return nil - } - return testnet3Addr -} - // ---------------------------------------------------------------------------- // methods to control the mock for testing // ---------------------------------------------------------------------------- diff --git a/zetaclient/tss/tss_signer.go b/zetaclient/tss/tss_signer.go index a4c881e3fd..0c7daa98e7 100644 --- a/zetaclient/tss/tss_signer.go +++ b/zetaclient/tss/tss_signer.go @@ -83,10 +83,6 @@ type TSS struct { Signers []string ZetacoreClient interfaces.ZetacoreClient KeysignsTracker *ConcurrentKeysignsTracker - - // TODO: support multiple Bitcoin network, not just one network - // https://github.com/zeta-chain/node/issues/1397 - BitcoinChainID int64 } // NewTSS creates a new TSS instance which can be used to sign transactions @@ -94,7 +90,6 @@ func NewTSS( ctx context.Context, client interfaces.ZetacoreClient, tssHistoricalList []observertypes.TSS, - bitcoinChainID int64, hotkeyPassword string, tssServer *tss.TssServer, ) (*TSS, error) { @@ -111,7 +106,6 @@ func NewTSS( logger: logger, ZetacoreClient: client, KeysignsTracker: NewKeysignsTracker(logger), - BitcoinChainID: bitcoinChainID, } err = newTss.LoadTssFilesFromDirectory(app.Config().TssPath) @@ -419,17 +413,28 @@ func (tss *TSS) SignBatch( return sigBytes, nil } -// Validate validates the TSS -func (tss *TSS) Validate() error { +// 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("invalid evm address : %s", evmAddress.String()) + return fmt.Errorf("blank tss evm address: %s", evmAddress.String()) } + logger.Info().Msgf("tss.eth: %s", evmAddress.String()) - if tss.BTCAddressWitnessPubkeyHash() == nil { - return fmt.Errorf("invalid btc pub key hash : %s", tss.BTCAddress()) + // 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 @@ -459,23 +464,13 @@ func (tss *TSS) EVMAddressList() []ethcommon.Address { } // BTCAddress generates a bech32 p2wpkh address from pubkey -func (tss *TSS) BTCAddress() string { - addr, err := GetTssAddrBTC(tss.CurrentPubkey, tss.BitcoinChainID) - if err != nil { - log.Error().Err(err).Msg("getKeyAddr error") - return "" - } - return addr -} - -// BTCAddressWitnessPubkeyHash generates a bech32 p2wpkh address from pubkey -func (tss *TSS) BTCAddressWitnessPubkeyHash() *btcutil.AddressWitnessPubKeyHash { - addrWPKH, err := getKeyAddrBTCWitnessPubkeyHash(tss.CurrentPubkey, tss.BitcoinChainID) +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 + return nil, err } - return addrWPKH + return addrWPKH, nil } // PubKeyCompressedBytes returns the compressed bytes of the current pubkey @@ -562,17 +557,6 @@ func (tss *TSS) LoadTssFilesFromDirectory(tssPath string) error { return nil } -// GetTssAddrBTC generates a bech32 p2wpkh address from pubkey -func GetTssAddrBTC(tssPubkey string, bitcoinChainID int64) (string, error) { - addrWPKH, err := getKeyAddrBTCWitnessPubkeyHash(tssPubkey, bitcoinChainID) - if err != nil { - log.Fatal().Err(err) - return "", err - } - - return addrWPKH.EncodeAddress(), nil -} - // GetTssAddrEVM generates an EVM address from pubkey func GetTssAddrEVM(tssPubkey string) (ethcommon.Address, error) { var keyAddr ethcommon.Address diff --git a/zetaclient/tss/tss_signer_test.go b/zetaclient/tss/tss_signer_test.go index e6ffb8262f..8db9e64704 100644 --- a/zetaclient/tss/tss_signer_test.go +++ b/zetaclient/tss/tss_signer_test.go @@ -8,11 +8,15 @@ import ( "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() { @@ -91,3 +95,129 @@ func GenerateKeyshareFiles(n int, dir string) error { } 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) + } + }) + } +}