diff --git a/zetaclient/bitcoin/bitcoin_client_rpc_test.go b/zetaclient/bitcoin/bitcoin_client_live_test.go similarity index 78% rename from zetaclient/bitcoin/bitcoin_client_rpc_test.go rename to zetaclient/bitcoin/bitcoin_client_live_test.go index b1f154d807..f77d7ac490 100644 --- a/zetaclient/bitcoin/bitcoin_client_rpc_test.go +++ b/zetaclient/bitcoin/bitcoin_client_live_test.go @@ -9,13 +9,6 @@ import ( "testing" "time" - "github.com/zeta-chain/zetacore/pkg/chains" - appcontext "github.com/zeta-chain/zetacore/zetaclient/app_context" - clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" - "github.com/zeta-chain/zetacore/zetaclient/config" - corecontext "github.com/zeta-chain/zetacore/zetaclient/core_context" - "github.com/zeta-chain/zetacore/zetaclient/interfaces" - "github.com/btcsuite/btcd/btcjson" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -25,12 +18,18 @@ import ( "github.com/rs/zerolog/log" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "github.com/zeta-chain/zetacore/pkg/chains" + appcontext "github.com/zeta-chain/zetacore/zetaclient/app_context" + clientcommon "github.com/zeta-chain/zetacore/zetaclient/common" + "github.com/zeta-chain/zetacore/zetaclient/config" + corecontext "github.com/zeta-chain/zetacore/zetaclient/core_context" + "github.com/zeta-chain/zetacore/zetaclient/interfaces" "github.com/zeta-chain/zetacore/zetaclient/testutils" ) type BitcoinClientTestSuite struct { suite.Suite - BitcoinChainClient *BTCChainClient + rpcClient *rpcclient.Client } func (suite *BitcoinClientTestSuite) SetupTest() { @@ -50,7 +49,8 @@ func (suite *BitcoinClientTestSuite) SetupTest() { client, err := NewBitcoinClient(appContext, chains.BtcRegtestChain(), nil, tss, tempSQLiteDbPath, clientcommon.DefaultLoggers(), config.BTCConfig{}, nil) suite.Require().NoError(err) - suite.BitcoinChainClient = client + suite.rpcClient, err = getRPCClient(18332) + suite.Require().NoError(err) skBytes, err := hex.DecodeString(skHex) suite.Require().NoError(err) suite.T().Logf("skBytes: %d", len(skBytes)) @@ -127,10 +127,10 @@ func getFeeRate(client *rpcclient.Client, confTarget int64, estimateMode *btcjso // All methods that begin with "Test" are run as tests within a // suite. func (suite *BitcoinClientTestSuite) Test1() { - feeResult, err := suite.BitcoinChainClient.rpcClient.EstimateSmartFee(1, nil) + feeResult, err := suite.rpcClient.EstimateSmartFee(1, nil) suite.Require().NoError(err) suite.T().Logf("fee result: %f", *feeResult.FeeRate) - bn, err := suite.BitcoinChainClient.rpcClient.GetBlockCount() + bn, err := suite.rpcClient.GetBlockCount() suite.Require().NoError(err) suite.T().Logf("block %d", bn) @@ -139,14 +139,13 @@ func (suite *BitcoinClientTestSuite) Test1() { err = chainhash.Decode(&hash, hashStr) suite.Require().NoError(err) - //:= suite.BitcoinChainClient.rpcClient.GetBlock(&hash) - block, err := suite.BitcoinChainClient.rpcClient.GetBlockVerboseTx(&hash) + block, err := suite.rpcClient.GetBlockVerboseTx(&hash) suite.Require().NoError(err) suite.T().Logf("block confirmation %d", block.Confirmations) suite.T().Logf("block txs len %d", len(block.Tx)) inTxs, err := FilterAndParseIncomingTx( - suite.BitcoinChainClient.rpcClient, + suite.rpcClient, block.Tx, uint64(block.Height), "tb1qsa222mn2rhdq9cruxkz8p2teutvxuextx3ees2", @@ -176,15 +175,14 @@ func (suite *BitcoinClientTestSuite) Test2() { err := chainhash.Decode(&hash, hashStr) suite.Require().NoError(err) - //:= suite.BitcoinChainClient.rpcClient.GetBlock(&hash) - block, err := suite.BitcoinChainClient.rpcClient.GetBlockVerboseTx(&hash) + block, err := suite.rpcClient.GetBlockVerboseTx(&hash) suite.Require().NoError(err) suite.T().Logf("block confirmation %d", block.Confirmations) suite.T().Logf("block height %d", block.Height) suite.T().Logf("block txs len %d", len(block.Tx)) inTxs, err := FilterAndParseIncomingTx( - suite.BitcoinChainClient.rpcClient, + suite.rpcClient, block.Tx, uint64(block.Height), "tb1qsa222mn2rhdq9cruxkz8p2teutvxuextx3ees2", @@ -197,7 +195,7 @@ func (suite *BitcoinClientTestSuite) Test2() { } func (suite *BitcoinClientTestSuite) Test3() { - client := suite.BitcoinChainClient.rpcClient + client := suite.rpcClient res, err := client.EstimateSmartFee(1, &btcjson.EstimateModeConservative) suite.Require().NoError(err) suite.T().Logf("fee: %f", *res.FeeRate) @@ -212,11 +210,18 @@ func (suite *BitcoinClientTestSuite) Test3() { suite.T().Logf("block number %d", bn) } -// func TestBitcoinChainClient(t *testing.T) { -// suite.Run(t, new(BitcoinClientTestSuite)) -// } +// TestBitcoinClientLive is a phony test to run each live test individually +func TestBitcoinClientLive(t *testing.T) { + // suite.Run(t, new(BitcoinClientTestSuite)) + + // LiveTestBitcoinFeeRate(t) + // LiveTestAvgFeeRateMainnetMempoolSpace(t) + // LiveTestAvgFeeRateTestnetMempoolSpace(t) + LiveTestGetSenderByVin(t) +} -// Remove prefix "Live" to run this live test +// LiveTestBitcoinFeeRate query Bitcoin mainnet fee rate every 5 minutes +// and compares Conservative and Economical fee rates for different block targets (1 and 2) func LiveTestBitcoinFeeRate(t *testing.T) { // setup Bitcoin client client, err := getRPCClient(8332) @@ -321,7 +326,7 @@ func compareAvgFeeRate(t *testing.T, client *rpcclient.Client, startBlock int, e } } -// Remove prefix "Live" to run this live test +// LiveTestAvgFeeRateMainnetMempoolSpace compares calculated fee rate with mempool.space fee rate for mainnet func LiveTestAvgFeeRateMainnetMempoolSpace(t *testing.T) { // setup Bitcoin client client, err := getRPCClient(8332) @@ -335,7 +340,7 @@ func LiveTestAvgFeeRateMainnetMempoolSpace(t *testing.T) { compareAvgFeeRate(t, client, startBlock, endBlock, false) } -// Remove prefix "Live" to run this live test +// LiveTestAvgFeeRateTestnetMempoolSpace compares calculated fee rate with mempool.space fee rate for testnet func LiveTestAvgFeeRateTestnetMempoolSpace(t *testing.T) { // setup Bitcoin client client, err := getRPCClient(18332) @@ -348,3 +353,76 @@ func LiveTestAvgFeeRateTestnetMempoolSpace(t *testing.T) { compareAvgFeeRate(t, client, startBlock, endBlock, true) } + +// LiveTestGetSenderByVin gets sender address for each vin and compares with mempool.space sender address +func LiveTestGetSenderByVin(t *testing.T) { + // setup Bitcoin client + chainID := int64(8332) + client, err := getRPCClient(chainID) + require.NoError(t, err) + + // net params + net, err := chains.GetBTCChainParams(chainID) + require.NoError(t, err) + testnet := false + if chainID == chains.BtcTestNetChain().ChainId { + testnet = true + } + + // calculates block range to test + startBlock, err := client.GetBlockCount() + require.NoError(t, err) + endBlock := startBlock - 5000 + + // loop through mempool.space blocks in descending order +BLOCKLOOP: + for bn := startBlock; bn >= endBlock; { + // get block hash + blkHash, err := client.GetBlockHash(int64(bn)) + if err != nil { + fmt.Printf("error GetBlockHash for block %d: %s\n", bn, err) + time.Sleep(3 * time.Second) + continue + } + + // get mempool.space txs for the block + mempoolTxs, err := testutils.GetBlockTxs(context.Background(), blkHash.String(), testnet) + if err != nil { + fmt.Printf("error GetBlockTxs %d: %s\n", bn, err) + time.Sleep(10 * time.Second) + continue + } + + // loop through each tx in the block + for i, mptx := range mempoolTxs { + // sample 10 txs per block + if i >= 10 { + break + } + for _, mpvin := range mptx.Vin { + // skip coinbase tx + if mpvin.IsCoinbase { + continue + } + // get sender address for each vin + vin := btcjson.Vin{ + Txid: mpvin.TxID, + Vout: mpvin.Vout, + } + senderAddr, err := GetSenderAddressByVin(client, vin, net) + if err != nil { + fmt.Printf("error GetSenderAddressByVin for block %d, tx %s vout %d: %s\n", bn, vin.Txid, vin.Vout, err) + time.Sleep(3 * time.Second) + continue BLOCKLOOP // retry the block + } + if senderAddr != mpvin.Prevout.ScriptpubkeyAddress { + panic(fmt.Sprintf("block %d, tx %s, vout %d: want %s, got %s\n", bn, vin.Txid, vin.Vout, mpvin.Prevout.ScriptpubkeyAddress, senderAddr)) + } else { + fmt.Printf("block: %d sender address type: %s\n", bn, mpvin.Prevout.ScriptpubkeyType) + } + } + } + bn-- + time.Sleep(500 * time.Millisecond) + } +} diff --git a/zetaclient/testutils/mempool_client.go b/zetaclient/testutils/mempool_client.go index 09b48b8a53..03c79120dc 100644 --- a/zetaclient/testutils/mempool_client.go +++ b/zetaclient/testutils/mempool_client.go @@ -9,8 +9,10 @@ import ( ) const ( - APIURLBlocks = "https://mempool.space/api/v1/blocks" - APIUrlBlocksTestnet = "https://mempool.space/testnet/api/v1/blocks" + APIURLBlocks = "https://mempool.space/api/v1/blocks" + APIURLBlockTxs = "https://mempool.space/api/block/%s/txs" + APIURLBlocksTestnet = "https://mempool.space/testnet/api/v1/blocks" + APIURLBlockTxsTestnet = "https://mempool.space/testnet/api/block/%s/txs" ) type MempoolBlock struct { @@ -30,6 +32,39 @@ type MempoolBlock struct { Extras BlockExtra `json:"extras"` } +type Vin struct { + TxID string `json:"txid"` + Vout uint32 `json:"vout"` + Prevout struct { + Scriptpubkey string `json:"scriptpubkey"` + ScriptpubkeyAsm string `json:"scriptpubkey_asm"` + ScriptpubkeyType string `json:"scriptpubkey_type"` + ScriptpubkeyAddress string `json:"scriptpubkey_address"` + Value int64 `json:"value"` + } `json:"prevout"` + Scriptsig string `json:"scriptsig"` + IsCoinbase bool `json:"is_coinbase"` + Sequence uint32 `json:"sequence"` +} + +type Vout struct { + Scriptpubkey string `json:"scriptpubkey"` + ScriptpubkeyAsm string `json:"scriptpubkey_asm"` + ScriptpubkeyType string `json:"scriptpubkey_type"` + Value int64 `json:"value"` +} + +type MempoolTx struct { + TxID string `json:"txid"` + Version int `json:"version"` + LockTime int `json:"locktime"` + Vin []Vin `json:"vin"` + Vout []Vout `json:"vout"` + Size int `json:"size"` + Weight int `json:"weight"` + Fee int `json:"fee"` +} + type BlockExtra struct { TotalFees int `json:"totalFees"` MedianFee float64 `json:"medianFee"` @@ -91,10 +126,11 @@ func Get(ctx context.Context, path string, v interface{}) error { return json.NewDecoder(r.Body).Decode(v) } +// GetBlocks returns return 15 mempool.space blocks [n-14, n] per request func GetBlocks(ctx context.Context, n int, testnet bool) ([]MempoolBlock, error) { path := fmt.Sprintf("%s/%d", APIURLBlocks, n) if testnet { - path = fmt.Sprintf("%s/%d", APIUrlBlocksTestnet, n) + path = fmt.Sprintf("%s/%d", APIURLBlocksTestnet, n) } blocks := make([]MempoolBlock, 0) if err := Get(ctx, path, &blocks); err != nil { @@ -102,3 +138,16 @@ func GetBlocks(ctx context.Context, n int, testnet bool) ([]MempoolBlock, error) } return blocks, nil } + +// GetBlockTxs a list of transactions in the block (up to 25 transactions beginning at index 0) +func GetBlockTxs(ctx context.Context, blockHash string, testnet bool) ([]MempoolTx, error) { + path := fmt.Sprintf(APIURLBlockTxs, blockHash) + if testnet { + path = fmt.Sprintf(APIURLBlockTxsTestnet, blockHash) + } + txs := make([]MempoolTx, 0) + if err := Get(ctx, path, &txs); err != nil { + return nil, err + } + return txs, nil +}