Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: derive Bitcoin TSS address by chain ID; added more Signet static info in 'chains' package #2907

Merged
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

### Refactor

Expand Down
33 changes: 12 additions & 21 deletions cmd/zetaclientd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -280,16 +268,19 @@ 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
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 {
Expand Down
3 changes: 2 additions & 1 deletion e2e/runner/bitcoin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
42 changes: 21 additions & 21 deletions pkg/chains/bitcoin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved
}

// IsBitcoinRegnet returns true if the chain id is for the regnet
Expand Down
17 changes: 10 additions & 7 deletions pkg/chains/bitcoin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
}

Expand All @@ -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)
}
})
}
Expand All @@ -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},
}

Expand Down
5 changes: 3 additions & 2 deletions x/observer/keeper/grpc_query_tss.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
18 changes: 13 additions & 5 deletions zetaclient/chains/base/observer.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,18 @@ func (ob *Observer) WithTSS(tss interfaces.TSSSigner) *Observer {
return ob
}

// TSSAddress returns the TSS address for the chain.
//
// Note: all chains uses TSS EVM address except Bitcoin chain.
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved
func (ob *Observer) TSSAddress() string {
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved
switch ob.chain.Consensus {
case chains.Consensus_bitcoin:
return ob.tss.BTCAddress(ob.Chain().ChainId).EncodeAddress()
default:
return ob.tss.EVMAddress().String()
}
}

// LastBlock get external last block height.
func (ob *Observer) LastBlock() uint64 {
return atomic.LoadUint64(&ob.lastBlock)
Expand Down Expand Up @@ -286,11 +298,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.TSSAddress()
return fmt.Sprintf("%d-%s-%d", ob.chain.ChainId, tssAddress, nonce)
}

Expand Down
47 changes: 43 additions & 4 deletions zetaclient/chains/base/observer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
)

Expand Down Expand Up @@ -271,6 +274,45 @@ func TestObserverGetterAndSetter(t *testing.T) {
})
}

func TestTSSAddress(t *testing.T) {
testConfig := sdk.GetConfig()
testConfig.SetBech32PrefixForAccount(cmd.Bech32PrefixAccAddr, cmd.Bech32PrefixAccPub)

tests := []struct {
name string
chain chains.Chain
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,
},
}

// run tests
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// create observer
ob := createObserver(t, tt.chain, defaultAlertLatency)

// get TSS address
addr := ob.TSSAddress()
require.Equal(t, tt.addrExpected, addr)
})
}
}

func TestIsBlockConfirmed(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -348,10 +390,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.TSSAddress(), tt.nonce)
require.Equal(t, exepctedID, outboundID)
})
}
Expand Down
2 changes: 1 addition & 1 deletion zetaclient/chains/bitcoin/observer/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.TSSAddress()

// #nosec G115 always positive
events, err := FilterAndParseIncomingTx(
Expand Down
9 changes: 4 additions & 5 deletions zetaclient/chains/bitcoin/observer/observer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
if err != nil {
return fmt.Errorf("btc: error decoding wallet address (%s) : %s", tssAddr, err.Error())
tssAddr := ob.TSS().BTCAddress(ob.Chain().ChainId)
if tssAddr == nil {
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
}
Expand Down
6 changes: 3 additions & 3 deletions zetaclient/chains/bitcoin/observer/outbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.TSSAddress()
amount := chains.NonceMarkAmount(nonce)
for i, utxo := range ob.utxos {
sats, err := bitcoin.GetSatoshis(utxo.Amount)
Expand Down Expand Up @@ -599,7 +599,7 @@ func (ob *Observer) checkTSSVout(params *crosschaintypes.OutboundParams, vouts [
}

nonce := params.TssNonce
tssAddress := ob.TSS().BTCAddress()
tssAddress := ob.TSSAddress()
for _, vout := range vouts {
// decode receiver and amount from vout
receiverExpected := tssAddress
Expand Down Expand Up @@ -658,7 +658,7 @@ func (ob *Observer) checkTSSVoutCancelled(params *crosschaintypes.OutboundParams
}

nonce := params.TssNonce
tssAddress := ob.TSS().BTCAddress()
tssAddress := ob.TSSAddress()
for _, vout := range vouts {
// decode receiver and amount from vout
receiverVout, amount, err := bitcoin.DecodeTSSVout(vout, tssAddress, ob.Chain())
Expand Down
4 changes: 2 additions & 2 deletions zetaclient/chains/bitcoin/observer/outbound_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func createObserverWithPrivateKey(t *testing.T) *Observer {
func createObserverWithUTXOs(t *testing.T) *Observer {
// Create Bitcoin observer
ob := createObserverWithPrivateKey(t)
tssAddress := ob.TSS().BTCAddressWitnessPubkeyHash().EncodeAddress()
tssAddress := ob.TSS().BTCAddress(chains.BitcoinTestnet.ChainId).EncodeAddress()

// Create 10 dummy UTXOs (22.44 BTC in total)
ob.utxos = make([]btcjson.ListUnspentResult, 0, 10)
Expand All @@ -78,7 +78,7 @@ func mineTxNSetNonceMark(ob *Observer, nonce uint64, txid string, preMarkIndex i
ob.includedTxResults[outboundID] = &btcjson.GetTransactionResult{TxID: txid}

// Set nonce mark
tssAddress := ob.TSS().BTCAddressWitnessPubkeyHash().EncodeAddress()
tssAddress := ob.TSS().BTCAddress(chains.BitcoinTestnet.ChainId).EncodeAddress()
nonceMark := btcjson.ListUnspentResult{
TxID: txid,
Address: tssAddress,
Expand Down
2 changes: 1 addition & 1 deletion zetaclient/chains/bitcoin/observer/rpc_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 := ob.TSS().BTCAddressWitnessPubkeyHash()
tssAddress := ob.TSS().BTCAddress(ob.Chain().ChainId)
blockTime, err := rpc.CheckRPCStatus(ob.btcClient, tssAddress)
if err != nil {
ob.Logger().Chain.Error().Err(err).Msg("CheckRPCStatus failed")
Expand Down
Loading
Loading