From 161c6b2a1bcafd53bdad847815eed152a27191f3 Mon Sep 17 00:00:00 2001 From: kevinssgh Date: Sun, 14 Apr 2024 22:57:46 -0400 Subject: [PATCH] add bitcoin support and unit tests --- cmd/zetaclientd/debug.go | 12 +- cmd/zetaclientd/start_utils.go | 14 +- zetaclient/bitcoin/bitcoin_client.go | 22 +- zetaclient/bitcoin/bitcoin_rpc_fallback.go | 363 ++++++++++++++++++ .../bitcoin/bitcoin_rpc_fallback_test.go | 137 +++++++ zetaclient/bitcoin/bitcoin_signer.go | 11 +- zetaclient/common/client_queue_test.go | 3 +- zetaclient/common/logger_test.go | 3 +- zetaclient/config/config_chain.go | 12 +- zetaclient/config/types.go | 8 +- zetaclient/evm/evm_rpc_fallback_test.go | 175 +++++++++ zetaclient/testutils/stub/btc_rpc.go | 41 +- zetaclient/testutils/stub/evm_rpc.go | 54 +-- 13 files changed, 759 insertions(+), 96 deletions(-) create mode 100644 zetaclient/bitcoin/bitcoin_rpc_fallback.go create mode 100644 zetaclient/bitcoin/bitcoin_rpc_fallback_test.go create mode 100644 zetaclient/evm/evm_rpc_fallback_test.go diff --git a/cmd/zetaclientd/debug.go b/cmd/zetaclientd/debug.go index 43d03dce5b..621c59a2bd 100644 --- a/cmd/zetaclientd/debug.go +++ b/cmd/zetaclientd/debug.go @@ -8,7 +8,6 @@ import ( "strings" "sync" - "github.com/btcsuite/btcd/rpcclient" sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" @@ -177,16 +176,7 @@ func DebugCmd() *cobra.Command { obBtc.WithZetaClient(bridge) obBtc.WithLogger(chainLogger) obBtc.WithChain(*chains.GetChainFromChainID(chainID)) - connCfg := &rpcclient.ConnConfig{ - Host: cfg.BitcoinConfig.RPCHost, - User: cfg.BitcoinConfig.RPCUsername, - Pass: cfg.BitcoinConfig.RPCPassword, - HTTPPostMode: true, - DisableTLS: true, - Params: cfg.BitcoinConfig.RPCParams, - } - - btcClient, err := rpcclient.New(connCfg, nil) + btcClient, err := bitcoin.NewRPCClientFallback(cfg.BitcoinConfig, chainLogger) if err != nil { return err } diff --git a/cmd/zetaclientd/start_utils.go b/cmd/zetaclientd/start_utils.go index 659287b675..0b440de7ae 100644 --- a/cmd/zetaclientd/start_utils.go +++ b/cmd/zetaclientd/start_utils.go @@ -78,11 +78,10 @@ func maskCfg(cfg config.Config) string { HsmHotKey: cfg.HsmHotKey, } + endpoints := make([]config.BTCEndpoint, len(cfg.BitcoinConfig.Endpoints)) + copy(endpoints, cfg.BitcoinConfig.Endpoints) maskedCfg.BitcoinConfig = config.BTCConfig{ - RPCUsername: cfg.BitcoinConfig.RPCUsername, - RPCPassword: cfg.BitcoinConfig.RPCPassword, - RPCHost: cfg.BitcoinConfig.RPCHost, - RPCParams: cfg.BitcoinConfig.RPCParams, + Endpoints: endpoints, } restrictedAddresses := make([]string, len(cfg.ComplianceConfig.RestrictedAddresses)) @@ -116,8 +115,11 @@ func maskCfg(cfg config.Config) string { chain.Endpoints[i] = endpointURL.Hostname() } } - maskedCfg.BitcoinConfig.RPCUsername = "" - maskedCfg.BitcoinConfig.RPCPassword = "" + + for i := range maskedCfg.BitcoinConfig.Endpoints { + maskedCfg.BitcoinConfig.Endpoints[i].RPCUsername = "" + maskedCfg.BitcoinConfig.Endpoints[i].RPCPassword = "" + } return maskedCfg.String() } diff --git a/zetaclient/bitcoin/bitcoin_client.go b/zetaclient/bitcoin/bitcoin_client.go index 110c89c474..0b4545d5dd 100644 --- a/zetaclient/bitcoin/bitcoin_client.go +++ b/zetaclient/bitcoin/bitcoin_client.go @@ -16,7 +16,6 @@ import ( "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" ethcommon "github.com/ethereum/go-ethereum/common" @@ -108,7 +107,7 @@ func (ob *BTCChainClient) WithLogger(logger zerolog.Logger) { } } -func (ob *BTCChainClient) WithBtcClient(client *rpcclient.Client) { +func (ob *BTCChainClient) WithBtcClient(client interfaces.BTCRPCClient) { ob.Mu.Lock() defer ob.Mu.Unlock() ob.rpcClient = client @@ -175,24 +174,11 @@ func NewBitcoinClient( return nil, fmt.Errorf("btc chains params not initialized") } ob.params = *chainParams + // initialize the Client - ob.logger.Chain.Info().Msgf("Chain %s endpoint %s", ob.chain.String(), btcCfg.RPCHost) - connCfg := &rpcclient.ConnConfig{ - Host: btcCfg.RPCHost, - User: btcCfg.RPCUsername, - Pass: btcCfg.RPCPassword, - HTTPPostMode: true, - DisableTLS: true, - Params: btcCfg.RPCParams, - } - client, err := rpcclient.New(connCfg, nil) + ob.rpcClient, err = NewRPCClientFallback(btcCfg, ob.logger.Chain) if err != nil { - return nil, fmt.Errorf("error creating rpc client: %s", err) - } - ob.rpcClient = client - err = client.Ping() - if err != nil { - return nil, fmt.Errorf("error ping the bitcoin server: %s", err) + return nil, err } ob.BlockCache, err = lru.New(btcBlocksPerDay) diff --git a/zetaclient/bitcoin/bitcoin_rpc_fallback.go b/zetaclient/bitcoin/bitcoin_rpc_fallback.go new file mode 100644 index 0000000000..e9961a51f9 --- /dev/null +++ b/zetaclient/bitcoin/bitcoin_rpc_fallback.go @@ -0,0 +1,363 @@ +package bitcoin + +import ( + "errors" + "fmt" + + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/rpcclient" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/rs/zerolog" + "github.com/zeta-chain/zetacore/zetaclient/common" + "github.com/zeta-chain/zetacore/zetaclient/config" + "github.com/zeta-chain/zetacore/zetaclient/interfaces" +) + +var _ interfaces.BTCRPCClient = &RPCClientFallback{} + +type RPCClientFallback struct { + btcConfig config.BTCConfig + rpcClients *common.ClientQueue + logger zerolog.Logger +} + +func NewRPCClientFallback(cfg config.BTCConfig, logger zerolog.Logger) (*RPCClientFallback, error) { + if len(cfg.Endpoints) == 0 { + return nil, errors.New("invalid endpoints") + } + rpcClientFallback := RPCClientFallback{ + btcConfig: cfg, + rpcClients: common.NewClientQueue(), + logger: logger, + } + for _, client := range cfg.Endpoints { + logger.Info().Msgf("endpoint %s", client.RPCHost) + connCfg := &rpcclient.ConnConfig{ + Host: client.RPCHost, + User: client.RPCUsername, + Pass: client.RPCPassword, + HTTPPostMode: true, + DisableTLS: true, + Params: client.RPCParams, + } + + rpcClient, err := rpcclient.New(connCfg, nil) + if err != nil { + return nil, fmt.Errorf("error creating rpc client: %s", err) + } + err = rpcClient.Ping() + if err != nil { + return nil, fmt.Errorf("error ping the bitcoin server: %s", err) + } + rpcClientFallback.rpcClients.Append(rpcClient) + } + return &rpcClientFallback, nil +} + +func (R *RPCClientFallback) GetNetworkInfo() (*btcjson.GetNetworkInfoResult, error) { + var res *btcjson.GetNetworkInfoResult + var err error + + for i := 0; i < R.rpcClients.Length(); i++ { + if client := R.rpcClients.First(); client != nil { + res, err = client.(interfaces.BTCRPCClient).GetNetworkInfo() + } + if err != nil { + R.logger.Debug().Err(err).Msg("client endpoint failed attempting fallback client") + R.rpcClients.Next() + continue + } + break + } + return res, err +} + +func (R *RPCClientFallback) CreateWallet(name string, opts ...rpcclient.CreateWalletOpt) (*btcjson.CreateWalletResult, error) { + var res *btcjson.CreateWalletResult + var err error + + for i := 0; i < R.rpcClients.Length(); i++ { + if client := R.rpcClients.First(); client != nil { + res, err = client.(interfaces.BTCRPCClient).CreateWallet(name, opts...) + } + if err != nil { + R.logger.Debug().Err(err).Msg("client endpoint failed attempting fallback client") + R.rpcClients.Next() + continue + } + break + } + return res, err +} + +func (R *RPCClientFallback) GetNewAddress(account string) (btcutil.Address, error) { + var res btcutil.Address + var err error + + for i := 0; i < R.rpcClients.Length(); i++ { + if client := R.rpcClients.First(); client != nil { + res, err = client.(interfaces.BTCRPCClient).GetNewAddress(account) + } + if err != nil { + R.logger.Debug().Err(err).Msg("client endpoint failed attempting fallback client") + R.rpcClients.Next() + continue + } + break + } + return res, err +} + +func (R *RPCClientFallback) GenerateToAddress(numBlocks int64, address btcutil.Address, maxTries *int64) ([]*chainhash.Hash, error) { + var res []*chainhash.Hash + var err error + + for i := 0; i < R.rpcClients.Length(); i++ { + if client := R.rpcClients.First(); client != nil { + res, err = client.(interfaces.BTCRPCClient).GenerateToAddress(numBlocks, address, maxTries) + } + if err != nil { + R.logger.Debug().Err(err).Msg("client endpoint failed attempting fallback client") + R.rpcClients.Next() + continue + } + break + } + return res, err +} + +func (R *RPCClientFallback) GetBalance(account string) (btcutil.Amount, error) { + var res btcutil.Amount + var err error + + for i := 0; i < R.rpcClients.Length(); i++ { + if client := R.rpcClients.First(); client != nil { + res, err = client.(interfaces.BTCRPCClient).GetBalance(account) + } + if err != nil { + R.logger.Debug().Err(err).Msg("client endpoint failed attempting fallback client") + R.rpcClients.Next() + continue + } + break + } + return res, err +} + +func (R *RPCClientFallback) SendRawTransaction(tx *wire.MsgTx, allowHighFees bool) (*chainhash.Hash, error) { + var res *chainhash.Hash + var err error + + for i := 0; i < R.rpcClients.Length(); i++ { + if client := R.rpcClients.First(); client != nil { + res, err = client.(interfaces.BTCRPCClient).SendRawTransaction(tx, allowHighFees) + } + if err != nil { + R.logger.Debug().Err(err).Msg("client endpoint failed attempting fallback client") + R.rpcClients.Next() + continue + } + break + } + return res, err +} + +func (R *RPCClientFallback) ListUnspent() ([]btcjson.ListUnspentResult, error) { + var res []btcjson.ListUnspentResult + var err error + + for i := 0; i < R.rpcClients.Length(); i++ { + if client := R.rpcClients.First(); client != nil { + res, err = client.(interfaces.BTCRPCClient).ListUnspent() + } + if err != nil { + R.logger.Debug().Err(err).Msg("client endpoint failed attempting fallback client") + R.rpcClients.Next() + continue + } + break + } + return res, err +} + +func (R *RPCClientFallback) ListUnspentMinMaxAddresses(minConf int, maxConf int, addrs []btcutil.Address) ([]btcjson.ListUnspentResult, error) { + var res []btcjson.ListUnspentResult + var err error + + for i := 0; i < R.rpcClients.Length(); i++ { + if client := R.rpcClients.First(); client != nil { + res, err = client.(interfaces.BTCRPCClient).ListUnspentMinMaxAddresses(minConf, maxConf, addrs) + } + if err != nil { + R.logger.Debug().Err(err).Msg("client endpoint failed attempting fallback client") + R.rpcClients.Next() + continue + } + break + } + return res, err +} + +func (R *RPCClientFallback) EstimateSmartFee(confTarget int64, mode *btcjson.EstimateSmartFeeMode) (*btcjson.EstimateSmartFeeResult, error) { + var res *btcjson.EstimateSmartFeeResult + var err error + + for i := 0; i < R.rpcClients.Length(); i++ { + if client := R.rpcClients.First(); client != nil { + res, err = client.(interfaces.BTCRPCClient).EstimateSmartFee(confTarget, mode) + } + if err != nil { + R.logger.Debug().Err(err).Msg("client endpoint failed attempting fallback client") + R.rpcClients.Next() + continue + } + break + } + return res, err +} + +func (R *RPCClientFallback) GetTransaction(txHash *chainhash.Hash) (*btcjson.GetTransactionResult, error) { + var res *btcjson.GetTransactionResult + var err error + + for i := 0; i < R.rpcClients.Length(); i++ { + if client := R.rpcClients.First(); client != nil { + res, err = client.(interfaces.BTCRPCClient).GetTransaction(txHash) + } + if err != nil { + R.logger.Debug().Err(err).Msg("client endpoint failed attempting fallback client") + R.rpcClients.Next() + continue + } + break + } + return res, err +} + +func (R *RPCClientFallback) GetRawTransaction(txHash *chainhash.Hash) (*btcutil.Tx, error) { + var res *btcutil.Tx + var err error + + for i := 0; i < R.rpcClients.Length(); i++ { + if client := R.rpcClients.First(); client != nil { + res, err = client.(interfaces.BTCRPCClient).GetRawTransaction(txHash) + } + if err != nil { + R.logger.Debug().Err(err).Msg("client endpoint failed attempting fallback client") + R.rpcClients.Next() + continue + } + break + } + return res, err +} + +func (R *RPCClientFallback) GetRawTransactionVerbose(txHash *chainhash.Hash) (*btcjson.TxRawResult, error) { + var res *btcjson.TxRawResult + var err error + + for i := 0; i < R.rpcClients.Length(); i++ { + if client := R.rpcClients.First(); client != nil { + res, err = client.(interfaces.BTCRPCClient).GetRawTransactionVerbose(txHash) + } + if err != nil { + R.logger.Debug().Err(err).Msg("client endpoint failed attempting fallback client") + R.rpcClients.Next() + continue + } + break + } + return res, err +} + +func (R *RPCClientFallback) GetBlockCount() (int64, error) { + var res int64 + var err error + + for i := 0; i < R.rpcClients.Length(); i++ { + if client := R.rpcClients.First(); client != nil { + res, err = client.(interfaces.BTCRPCClient).GetBlockCount() + } + if err != nil { + R.logger.Debug().Err(err).Msg("client endpoint failed attempting fallback client") + R.rpcClients.Next() + continue + } + break + } + return res, err +} + +func (R *RPCClientFallback) GetBlockHash(blockHeight int64) (*chainhash.Hash, error) { + var res *chainhash.Hash + var err error + + for i := 0; i < R.rpcClients.Length(); i++ { + if client := R.rpcClients.First(); client != nil { + res, err = client.(interfaces.BTCRPCClient).GetBlockHash(blockHeight) + } + if err != nil { + R.logger.Debug().Err(err).Msg("client endpoint failed attempting fallback client") + R.rpcClients.Next() + continue + } + break + } + return res, err +} + +func (R *RPCClientFallback) GetBlockVerbose(blockHash *chainhash.Hash) (*btcjson.GetBlockVerboseResult, error) { + var res *btcjson.GetBlockVerboseResult + var err error + + for i := 0; i < R.rpcClients.Length(); i++ { + if client := R.rpcClients.First(); client != nil { + res, err = client.(interfaces.BTCRPCClient).GetBlockVerbose(blockHash) + } + if err != nil { + R.logger.Debug().Err(err).Msg("client endpoint failed attempting fallback client") + R.rpcClients.Next() + continue + } + break + } + return res, err +} + +func (R *RPCClientFallback) GetBlockVerboseTx(blockHash *chainhash.Hash) (*btcjson.GetBlockVerboseTxResult, error) { + var res *btcjson.GetBlockVerboseTxResult + var err error + + for i := 0; i < R.rpcClients.Length(); i++ { + if client := R.rpcClients.First(); client != nil { + res, err = client.(interfaces.BTCRPCClient).GetBlockVerboseTx(blockHash) + } + if err != nil { + R.logger.Debug().Err(err).Msg("client endpoint failed attempting fallback client") + R.rpcClients.Next() + continue + } + break + } + return res, err +} + +func (R *RPCClientFallback) GetBlockHeader(blockHash *chainhash.Hash) (*wire.BlockHeader, error) { + var res *wire.BlockHeader + var err error + + for i := 0; i < R.rpcClients.Length(); i++ { + if client := R.rpcClients.First(); client != nil { + res, err = client.(interfaces.BTCRPCClient).GetBlockHeader(blockHash) + } + if err != nil { + R.logger.Debug().Err(err).Msg("client endpoint failed attempting fallback client") + R.rpcClients.Next() + continue + } + break + } + return res, err +} diff --git a/zetaclient/bitcoin/bitcoin_rpc_fallback_test.go b/zetaclient/bitcoin/bitcoin_rpc_fallback_test.go new file mode 100644 index 0000000000..edc0583b25 --- /dev/null +++ b/zetaclient/bitcoin/bitcoin_rpc_fallback_test.go @@ -0,0 +1,137 @@ +package bitcoin + +import ( + "errors" + "testing" + + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/pkg/chains" + "github.com/zeta-chain/zetacore/zetaclient/common" + "github.com/zeta-chain/zetacore/zetaclient/config" + "github.com/zeta-chain/zetacore/zetaclient/testutils/stub" +) + +func setupBTCFallbackClient() *RPCClientFallback { + client1 := stub.NewMockBTCRPCClient() + client1.WithError(errors.New("rpc error")) + + client2 := stub.NewMockBTCRPCClient() + client2.WithError(nil) + + clientq := common.NewClientQueue() + clientq.Append(client1) + clientq.Append(client2) + + return &RPCClientFallback{ + btcConfig: config.BTCConfig{}, + rpcClients: clientq, + logger: zerolog.Logger{}, + } +} + +func TestRPCClientFallback_CreateWallet(t *testing.T) { + rpcClientFallback := setupBTCFallbackClient() + _, err := rpcClientFallback.CreateWallet("testWallet") + require.NoError(t, err) +} + +func TestRPCClientFallback_EstimateSmartFee(t *testing.T) { + rpcClientFallback := setupBTCFallbackClient() + _, err := rpcClientFallback.EstimateSmartFee(0, &btcjson.EstimateModeUnset) + require.NoError(t, err) +} + +func TestRPCClientFallback_GenerateToAddress(t *testing.T) { + rpcClientFallback := setupBTCFallbackClient() + _, err := rpcClientFallback.GenerateToAddress(0, &chains.AddressTaproot{}, nil) + require.NoError(t, err) +} + +func TestRPCClientFallback_GetBalance(t *testing.T) { + rpcClientFallback := setupBTCFallbackClient() + _, err := rpcClientFallback.GetBalance("") + require.NoError(t, err) +} + +func TestRPCClientFallback_GetBlockCount(t *testing.T) { + rpcClientFallback := setupBTCFallbackClient() + _, err := rpcClientFallback.GetBlockCount() + require.NoError(t, err) +} + +func TestRPCClientFallback_GetBlockHash(t *testing.T) { + rpcClientFallback := setupBTCFallbackClient() + _, err := rpcClientFallback.GetBlockHash(0) + require.NoError(t, err) +} + +func TestRPCClientFallback_GetBlockHeader(t *testing.T) { + rpcClientFallback := setupBTCFallbackClient() + _, err := rpcClientFallback.GetBlockHeader(&chainhash.Hash{}) + require.NoError(t, err) +} + +func TestRPCClientFallback_GetBlockVerbose(t *testing.T) { + rpcClientFallback := setupBTCFallbackClient() + _, err := rpcClientFallback.GetBlockVerbose(&chainhash.Hash{}) + require.NoError(t, err) +} + +func TestRPCClientFallback_GetBlockVerboseTx(t *testing.T) { + rpcClientFallback := setupBTCFallbackClient() + _, err := rpcClientFallback.GetBlockVerboseTx(&chainhash.Hash{}) + require.NoError(t, err) +} + +func TestRPCClientFallback_GetNetworkInfo(t *testing.T) { + rpcClientFallback := setupBTCFallbackClient() + _, err := rpcClientFallback.GetNetworkInfo() + require.NoError(t, err) +} + +func TestRPCClientFallback_GetNewAddress(t *testing.T) { + rpcClientFallback := setupBTCFallbackClient() + _, err := rpcClientFallback.GetNewAddress("Test") + require.NoError(t, err) +} + +func TestRPCClientFallback_GetRawTransaction(t *testing.T) { + rpcClientFallback := setupBTCFallbackClient() + _, err := rpcClientFallback.GetRawTransaction(&chainhash.Hash{}) + require.NoError(t, err) +} + +func TestRPCClientFallback_GetRawTransactionVerbose(t *testing.T) { + rpcClientFallback := setupBTCFallbackClient() + _, err := rpcClientFallback.GetRawTransactionVerbose(&chainhash.Hash{}) + require.NoError(t, err) +} + +func TestRPCClientFallback_GetTransaction(t *testing.T) { + rpcClientFallback := setupBTCFallbackClient() + _, err := rpcClientFallback.GetTransaction(&chainhash.Hash{}) + require.NoError(t, err) +} + +func TestRPCClientFallback_ListUnspentMinMaxAddresses(t *testing.T) { + rpcClientFallback := setupBTCFallbackClient() + _, err := rpcClientFallback.ListUnspentMinMaxAddresses(0, 0, []btcutil.Address{}) + require.NoError(t, err) +} + +func TestRPCClientFallback_ListUnspent(t *testing.T) { + rpcClientFallback := setupBTCFallbackClient() + _, err := rpcClientFallback.ListUnspent() + require.NoError(t, err) +} + +func TestRPCClientFallback_SendRawTransaction(t *testing.T) { + rpcClientFallback := setupBTCFallbackClient() + _, err := rpcClientFallback.SendRawTransaction(&wire.MsgTx{}, false) + require.NoError(t, err) +} diff --git a/zetaclient/bitcoin/bitcoin_signer.go b/zetaclient/bitcoin/bitcoin_signer.go index a5ff22fa6a..b66744d0bb 100644 --- a/zetaclient/bitcoin/bitcoin_signer.go +++ b/zetaclient/bitcoin/bitcoin_signer.go @@ -10,7 +10,6 @@ import ( "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/chaincfg/chainhash" - "github.com/btcsuite/btcd/rpcclient" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" @@ -56,15 +55,7 @@ func NewBTCSigner( loggers clientcommon.ClientLogger, ts *metrics.TelemetryServer, coreContext *corecontext.ZetaCoreContext) (*BTCSigner, error) { - connCfg := &rpcclient.ConnConfig{ - Host: cfg.RPCHost, - User: cfg.RPCUsername, - Pass: cfg.RPCPassword, - HTTPPostMode: true, - DisableTLS: true, - Params: cfg.RPCParams, - } - client, err := rpcclient.New(connCfg, nil) + client, err := NewRPCClientFallback(cfg, loggers.Std) if err != nil { return nil, fmt.Errorf("error creating bitcoin rpc client: %s", err) } diff --git a/zetaclient/common/client_queue_test.go b/zetaclient/common/client_queue_test.go index 31d6f75794..71a70f28f3 100644 --- a/zetaclient/common/client_queue_test.go +++ b/zetaclient/common/client_queue_test.go @@ -1,8 +1,9 @@ package common import ( - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) func setupClientQueue() *ClientQueue { diff --git a/zetaclient/common/logger_test.go b/zetaclient/common/logger_test.go index bb5547f9d1..59958317a4 100644 --- a/zetaclient/common/logger_test.go +++ b/zetaclient/common/logger_test.go @@ -1,8 +1,9 @@ package common import ( - "github.com/stretchr/testify/require" "testing" + + "github.com/stretchr/testify/require" ) func TestDefaultLoggers(t *testing.T) { diff --git a/zetaclient/config/config_chain.go b/zetaclient/config/config_chain.go index 8ac8f639cd..8a23678c2d 100644 --- a/zetaclient/config/config_chain.go +++ b/zetaclient/config/config_chain.go @@ -37,10 +37,14 @@ func New() Config { } var bitcoinConfigRegnet = BTCConfig{ - RPCUsername: "smoketest", // smoketest is the previous name for E2E test, we keep this name for compatibility between client versions in upgrade test - RPCPassword: "123", - RPCHost: "bitcoin:18443", - RPCParams: "regtest", + Endpoints: []BTCEndpoint{ + { + RPCUsername: "smoketest", // smoketest is the previous name for E2E test, we keep this name for compatibility between client versions in upgrade test + RPCPassword: "123", + RPCHost: "bitcoin:18443", + RPCParams: "regtest", + }, + }, } var evmChainsConfigs = map[int64]EVMConfig{ diff --git a/zetaclient/config/types.go b/zetaclient/config/types.go index 354e65b12b..2db55f6a12 100644 --- a/zetaclient/config/types.go +++ b/zetaclient/config/types.go @@ -32,7 +32,7 @@ type EVMConfig struct { Endpoints []string } -type BTCConfig struct { +type BTCEndpoint struct { // the following are rpcclient ConnConfig fields RPCUsername string RPCPassword string @@ -40,6 +40,10 @@ type BTCConfig struct { RPCParams string // "regtest", "mainnet", "testnet3" } +type BTCConfig struct { + Endpoints []BTCEndpoint +} + type ComplianceConfig struct { LogPath string `json:"LogPath"` RestrictedAddresses []string `json:"RestrictedAddresses"` @@ -108,7 +112,7 @@ func (c Config) GetBTCConfig() (BTCConfig, bool) { c.cfgLock.RLock() defer c.cfgLock.RUnlock() - return c.BitcoinConfig, c.BitcoinConfig != (BTCConfig{}) + return c.BitcoinConfig, c.BitcoinConfig.Endpoints != nil } func (c Config) String() string { diff --git a/zetaclient/evm/evm_rpc_fallback_test.go b/zetaclient/evm/evm_rpc_fallback_test.go new file mode 100644 index 0000000000..03c7f1fa2f --- /dev/null +++ b/zetaclient/evm/evm_rpc_fallback_test.go @@ -0,0 +1,175 @@ +package evm + +import ( + "context" + "errors" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum" + ethcommon "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/onrik/ethrpc" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + "github.com/zeta-chain/zetacore/testutil/sample" + "github.com/zeta-chain/zetacore/zetaclient/common" + "github.com/zeta-chain/zetacore/zetaclient/config" + "github.com/zeta-chain/zetacore/zetaclient/testutils/stub" +) + +func setupTestEVMClient() *EthClientFallback { + client1 := stub.NewMockEvmClient() + client1.WithError(errors.New("rpc error")) + client2 := stub.NewMockEvmClient() + + clientQ := common.NewClientQueue() + clientQ.Append(client1) + clientQ.Append(client2) + return &EthClientFallback{ + evmCfg: config.EVMConfig{}, + ethClients: clientQ, + jsonRPCClients: clientQ, + logger: zerolog.Logger{}, + } +} + +func TestEthClientFallback_BlockByNumber(t *testing.T) { + client := setupTestEVMClient() + resp, err := client.BlockByNumber(context.Background(), big.NewInt(443)) + require.NoError(t, err) + require.Equal(t, ethtypes.Block{}, *resp) +} + +func TestEthClientFallback_CallContract(t *testing.T) { + client := setupTestEVMClient() + _, err := client.CallContract(context.Background(), ethereum.CallMsg{}, big.NewInt(54)) + require.NoError(t, err) +} + +func TestEthClientFallback_BlockNumber(t *testing.T) { + client := setupTestEVMClient() + resp, err := client.BlockNumber(context.Background()) + require.NoError(t, err) + require.Equal(t, uint64(88), resp) +} + +func TestEthClientFallback_ChainID(t *testing.T) { +} + +func TestEthClientFallback_CodeAt(t *testing.T) { + client := setupTestEVMClient() + resp, err := client.CodeAt(context.Background(), sample.EthAddress(), big.NewInt(88)) + require.NoError(t, err) + require.Equal(t, []byte{}, resp) +} + +func TestEthClientFallback_EstimateGas(t *testing.T) { + client := setupTestEVMClient() + resp, err := client.EstimateGas(context.Background(), ethereum.CallMsg{}) + require.NoError(t, err) + require.Equal(t, uint64(0), resp) +} + +func TestEthClientFallback_EthGetBlockByNumber(t *testing.T) { + client := setupTestEVMClient() + var expected *ethrpc.Block + expected = nil + + resp, err := client.EthGetBlockByNumber(0, false) + require.ErrorContains(t, err, "no block found") + require.Equal(t, expected, resp) + +} + +func TestEthClientFallback_EthGetTransactionByHash(t *testing.T) { + client := setupTestEVMClient() + var expected *ethrpc.Transaction + expected = nil + + resp, err := client.EthGetTransactionByHash("") + require.ErrorContains(t, err, "no transaction found") + require.Equal(t, expected, resp) +} + +func TestEthClientFallback_FilterLogs(t *testing.T) { + client := setupTestEVMClient() + resp, err := client.FilterLogs(context.Background(), ethereum.FilterQuery{}) + require.NoError(t, err) + require.Equal(t, []ethtypes.Log{}, resp) +} + +func TestEthClientFallback_HeaderByNumber(t *testing.T) { + client := setupTestEVMClient() + resp, err := client.HeaderByNumber(context.Background(), big.NewInt(88)) + require.NoError(t, err) + require.Equal(t, ethtypes.Header{}, *resp) +} + +func TestEthClientFallback_PendingCodeAt(t *testing.T) { + client := setupTestEVMClient() + resp, err := client.PendingCodeAt(context.Background(), sample.EthAddress()) + require.NoError(t, err) + require.Equal(t, []byte{}, resp) +} + +func TestEthClientFallback_PendingNonceAt(t *testing.T) { + client := setupTestEVMClient() + resp, err := client.PendingNonceAt(context.Background(), sample.EthAddress()) + require.NoError(t, err) + require.Equal(t, uint64(0), resp) +} + +func TestEthClientFallback_SendTransaction(t *testing.T) { + client := setupTestEVMClient() + err := client.SendTransaction(context.Background(), ðtypes.Transaction{}) + require.NoError(t, err) +} + +func TestEthClientFallback_SubscribeFilterLogs(t *testing.T) { + client := setupTestEVMClient() + channel := make(chan<- ethtypes.Log) + + resp, err := client.SubscribeFilterLogs(context.Background(), ethereum.FilterQuery{}, channel) + require.NoError(t, err) + require.Equal(t, stub.Subscription{}, resp) +} + +func TestEthClientFallback_SuggestGasPrice(t *testing.T) { + client := setupTestEVMClient() + resp, err := client.SuggestGasPrice(context.Background()) + require.NoError(t, err) + require.Equal(t, big.NewInt(0), resp) +} + +func TestEthClientFallback_SuggestGasTipCap(t *testing.T) { + client := setupTestEVMClient() + resp, err := client.SuggestGasTipCap(context.Background()) + require.NoError(t, err) + require.Equal(t, big.NewInt(0), resp) +} + +func TestEthClientFallback_TransactionByHash(t *testing.T) { + client := setupTestEVMClient() + resp, isPending, err := client.TransactionByHash(context.Background(), sample.Hash()) + require.NoError(t, err) + require.Equal(t, false, isPending) + require.Equal(t, ethtypes.Transaction{}, *resp) +} + +func TestEthClientFallback_TransactionReceipt(t *testing.T) { + client := setupTestEVMClient() + var expected *ethtypes.Receipt + expected = nil + + resp, err := client.TransactionReceipt(context.Background(), sample.Hash()) + require.ErrorContains(t, err, "no receipt found") + require.Equal(t, expected, resp) +} + +func TestEthClientFallback_TransactionSender(t *testing.T) { + client := setupTestEVMClient() + resp, err := client.TransactionSender(context.Background(), ðtypes.Transaction{}, sample.Hash(), uint(0)) + require.NoError(t, err) + require.Equal(t, ethcommon.Address{}, resp) +} diff --git a/zetaclient/testutils/stub/btc_rpc.go b/zetaclient/testutils/stub/btc_rpc.go index 6b8ac84c53..6328792b82 100644 --- a/zetaclient/testutils/stub/btc_rpc.go +++ b/zetaclient/testutils/stub/btc_rpc.go @@ -1,8 +1,6 @@ package stub import ( - "errors" - "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/rpcclient" @@ -17,6 +15,7 @@ var _ interfaces.BTCRPCClient = &MockBTCRPCClient{} // MockBTCRPCClient is a mock implementation of the BTCRPCClient interface type MockBTCRPCClient struct { Txs []*btcutil.Tx + err error } // NewMockBTCRPCClient creates a new mock BTC RPC client @@ -32,43 +31,43 @@ func (c *MockBTCRPCClient) Reset() *MockBTCRPCClient { } func (c *MockBTCRPCClient) GetNetworkInfo() (*btcjson.GetNetworkInfoResult, error) { - return nil, errors.New("not implemented") + return nil, c.err } func (c *MockBTCRPCClient) CreateWallet(_ string, _ ...rpcclient.CreateWalletOpt) (*btcjson.CreateWalletResult, error) { - return nil, errors.New("not implemented") + return nil, c.err } func (c *MockBTCRPCClient) GetNewAddress(_ string) (btcutil.Address, error) { - return nil, errors.New("not implemented") + return nil, c.err } func (c *MockBTCRPCClient) GenerateToAddress(_ int64, _ btcutil.Address, _ *int64) ([]*chainhash.Hash, error) { - return nil, errors.New("not implemented") + return nil, c.err } func (c *MockBTCRPCClient) GetBalance(_ string) (btcutil.Amount, error) { - return 0, errors.New("not implemented") + return 0, c.err } func (c *MockBTCRPCClient) SendRawTransaction(_ *wire.MsgTx, _ bool) (*chainhash.Hash, error) { - return nil, errors.New("not implemented") + return nil, c.err } func (c *MockBTCRPCClient) ListUnspent() ([]btcjson.ListUnspentResult, error) { - return nil, errors.New("not implemented") + return nil, c.err } func (c *MockBTCRPCClient) ListUnspentMinMaxAddresses(_ int, _ int, _ []btcutil.Address) ([]btcjson.ListUnspentResult, error) { - return nil, errors.New("not implemented") + return nil, c.err } func (c *MockBTCRPCClient) EstimateSmartFee(_ int64, _ *btcjson.EstimateSmartFeeMode) (*btcjson.EstimateSmartFeeResult, error) { - return nil, errors.New("not implemented") + return nil, c.err } func (c *MockBTCRPCClient) GetTransaction(_ *chainhash.Hash) (*btcjson.GetTransactionResult, error) { - return nil, errors.New("not implemented") + return nil, c.err } // GetRawTransaction returns a pre-loaded transaction or nil @@ -79,31 +78,31 @@ func (c *MockBTCRPCClient) GetRawTransaction(_ *chainhash.Hash) (*btcutil.Tx, er c.Txs = c.Txs[:len(c.Txs)-1] return tx, nil } - return nil, errors.New("no transaction found") + return nil, c.err } func (c *MockBTCRPCClient) GetRawTransactionVerbose(_ *chainhash.Hash) (*btcjson.TxRawResult, error) { - return nil, errors.New("not implemented") + return nil, c.err } func (c *MockBTCRPCClient) GetBlockCount() (int64, error) { - return 0, errors.New("not implemented") + return 0, c.err } func (c *MockBTCRPCClient) GetBlockHash(_ int64) (*chainhash.Hash, error) { - return nil, errors.New("not implemented") + return nil, c.err } func (c *MockBTCRPCClient) GetBlockVerbose(_ *chainhash.Hash) (*btcjson.GetBlockVerboseResult, error) { - return nil, errors.New("not implemented") + return nil, c.err } func (c *MockBTCRPCClient) GetBlockVerboseTx(_ *chainhash.Hash) (*btcjson.GetBlockVerboseTxResult, error) { - return nil, errors.New("not implemented") + return nil, c.err } func (c *MockBTCRPCClient) GetBlockHeader(_ *chainhash.Hash) (*wire.BlockHeader, error) { - return nil, errors.New("not implemented") + return nil, c.err } // ---------------------------------------------------------------------------- @@ -119,3 +118,7 @@ func (c *MockBTCRPCClient) WithRawTransactions(txs []*btcutil.Tx) *MockBTCRPCCli c.Txs = append(c.Txs, txs...) return c } + +func (c *MockBTCRPCClient) WithError(err error) { + c.err = err +} diff --git a/zetaclient/testutils/stub/evm_rpc.go b/zetaclient/testutils/stub/evm_rpc.go index c36d91fcd4..f497fa9653 100644 --- a/zetaclient/testutils/stub/evm_rpc.go +++ b/zetaclient/testutils/stub/evm_rpc.go @@ -16,15 +16,15 @@ import ( const EVMRPCEnabled = "MockEVMRPCEnabled" // Subscription interface -var _ ethereum.Subscription = subscription{} +var _ ethereum.Subscription = Subscription{} -type subscription struct { +type Subscription struct { } -func (s subscription) Unsubscribe() { +func (s Subscription) Unsubscribe() { } -func (s subscription) Err() <-chan error { +func (s Subscription) Err() <-chan error { return nil } @@ -35,6 +35,12 @@ type MockEvmClient struct { Receipts []*ethtypes.Receipt Blocks []*ethrpc.Block Transactions []*ethrpc.Transaction + err error +} + +func NewMockEvmClient() *MockEvmClient { + client := &MockEvmClient{err: nil} + return client.Reset() } func (e *MockEvmClient) EthGetBlockByNumber(_ int, _ bool) (*ethrpc.Block, error) { @@ -57,67 +63,62 @@ func (e *MockEvmClient) EthGetTransactionByHash(_ string) (*ethrpc.Transaction, return nil, errors.New("no transaction found") } -func NewMockEvmClient() *MockEvmClient { - client := &MockEvmClient{} - return client.Reset() -} - func (e *MockEvmClient) SubscribeFilterLogs(_ context.Context, _ ethereum.FilterQuery, _ chan<- ethtypes.Log) (ethereum.Subscription, error) { - return subscription{}, nil + return Subscription{}, e.err } func (e *MockEvmClient) CodeAt(_ context.Context, _ ethcommon.Address, _ *big.Int) ([]byte, error) { - return []byte{}, nil + return []byte{}, e.err } func (e *MockEvmClient) CallContract(_ context.Context, _ ethereum.CallMsg, _ *big.Int) ([]byte, error) { - return []byte{}, nil + return []byte{}, e.err } func (e *MockEvmClient) HeaderByNumber(_ context.Context, _ *big.Int) (*ethtypes.Header, error) { - return ðtypes.Header{}, nil + return ðtypes.Header{}, e.err } func (e *MockEvmClient) PendingCodeAt(_ context.Context, _ ethcommon.Address) ([]byte, error) { - return []byte{}, nil + return []byte{}, e.err } func (e *MockEvmClient) PendingNonceAt(_ context.Context, _ ethcommon.Address) (uint64, error) { - return 0, nil + return 0, e.err } func (e *MockEvmClient) SuggestGasPrice(_ context.Context) (*big.Int, error) { - return big.NewInt(0), nil + return big.NewInt(0), e.err } func (e *MockEvmClient) SuggestGasTipCap(_ context.Context) (*big.Int, error) { - return big.NewInt(0), nil + return big.NewInt(0), e.err } func (e *MockEvmClient) EstimateGas(_ context.Context, _ ethereum.CallMsg) (gas uint64, err error) { gas = 0 - err = nil + err = e.err return } func (e *MockEvmClient) SendTransaction(_ context.Context, _ *ethtypes.Transaction) error { - return nil + return e.err } func (e *MockEvmClient) FilterLogs(_ context.Context, _ ethereum.FilterQuery) ([]ethtypes.Log, error) { - return []ethtypes.Log{}, nil + return []ethtypes.Log{}, e.err } func (e *MockEvmClient) BlockNumber(_ context.Context) (uint64, error) { - return 0, nil + return 88, e.err } func (e *MockEvmClient) BlockByNumber(_ context.Context, _ *big.Int) (*ethtypes.Block, error) { - return ðtypes.Block{}, nil + return ðtypes.Block{}, e.err } func (e *MockEvmClient) TransactionByHash(_ context.Context, _ ethcommon.Hash) (tx *ethtypes.Transaction, isPending bool, err error) { - return ðtypes.Transaction{}, false, nil + return ðtypes.Transaction{}, false, e.err } func (e *MockEvmClient) TransactionReceipt(_ context.Context, _ ethcommon.Hash) (*ethtypes.Receipt, error) { @@ -131,7 +132,7 @@ func (e *MockEvmClient) TransactionReceipt(_ context.Context, _ ethcommon.Hash) } func (e *MockEvmClient) TransactionSender(_ context.Context, _ *ethtypes.Transaction, _ ethcommon.Hash, _ uint) (ethcommon.Address, error) { - return ethcommon.Address{}, nil + return ethcommon.Address{}, e.err } func (e *MockEvmClient) Reset() *MockEvmClient { @@ -173,3 +174,8 @@ func (e *MockEvmClient) WithTransactions(txs []*ethrpc.Transaction) *MockEvmClie e.Transactions = append(e.Transactions, txs...) return e } + +func (e *MockEvmClient) WithError(err error) *MockEvmClient { + e.err = err + return e +}