github.com/trezor/blockbook@v0.4.1-0.20240328132726-e9a08582ee2c/tests/rpc/rpc.go (about)

     1  //go:build integration
     2  
     3  package rpc
     4  
     5  import (
     6  	"encoding/json"
     7  	"io/ioutil"
     8  	"path/filepath"
     9  	"reflect"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	mapset "github.com/deckarep/golang-set"
    15  	"github.com/juju/errors"
    16  	"github.com/trezor/blockbook/bchain"
    17  )
    18  
    19  var testMap = map[string]func(t *testing.T, th *TestHandler){
    20  	"GetBlockHash":             testGetBlockHash,
    21  	"GetBlock":                 testGetBlock,
    22  	"GetTransaction":           testGetTransaction,
    23  	"GetTransactionForMempool": testGetTransactionForMempool,
    24  	"MempoolSync":              testMempoolSync,
    25  	"EstimateSmartFee":         testEstimateSmartFee,
    26  	"EstimateFee":              testEstimateFee,
    27  	"GetBestBlockHash":         testGetBestBlockHash,
    28  	"GetBestBlockHeight":       testGetBestBlockHeight,
    29  	"GetBlockHeader":           testGetBlockHeader,
    30  }
    31  
    32  type TestHandler struct {
    33  	Chain    bchain.BlockChain
    34  	Mempool  bchain.Mempool
    35  	TestData *TestData
    36  }
    37  
    38  type TestData struct {
    39  	BlockHeight uint32                `json:"blockHeight"`
    40  	BlockHash   string                `json:"blockHash"`
    41  	BlockTime   int64                 `json:"blockTime"`
    42  	BlockSize   int                   `json:"blockSize"`
    43  	BlockTxs    []string              `json:"blockTxs"`
    44  	TxDetails   map[string]*bchain.Tx `json:"txDetails"`
    45  }
    46  
    47  func IntegrationTest(t *testing.T, coin string, chain bchain.BlockChain, mempool bchain.Mempool, testConfig json.RawMessage) {
    48  	tests, err := getTests(testConfig)
    49  	if err != nil {
    50  		t.Fatalf("Failed loading of test list: %s", err)
    51  	}
    52  
    53  	parser := chain.GetChainParser()
    54  	td, err := loadTestData(coin, parser)
    55  	if err != nil {
    56  		t.Fatalf("Failed loading of test data: %s", err)
    57  	}
    58  
    59  	h := TestHandler{
    60  		Chain:    chain,
    61  		Mempool:  mempool,
    62  		TestData: td,
    63  	}
    64  
    65  	for _, test := range tests {
    66  		if f, found := testMap[test]; found {
    67  			t.Run(test, func(t *testing.T) { f(t, &h) })
    68  		} else {
    69  			t.Errorf("%s: test not found", test)
    70  			continue
    71  		}
    72  	}
    73  }
    74  
    75  func getTests(cfg json.RawMessage) ([]string, error) {
    76  	var v []string
    77  	err := json.Unmarshal(cfg, &v)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  	if len(v) == 0 {
    82  		return nil, errors.New("No tests declared")
    83  	}
    84  	return v, nil
    85  }
    86  
    87  func loadTestData(coin string, parser bchain.BlockChainParser) (*TestData, error) {
    88  	path := filepath.Join("rpc/testdata", coin+".json")
    89  	b, err := ioutil.ReadFile(path)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  	var v TestData
    94  	err = json.Unmarshal(b, &v)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  	for _, tx := range v.TxDetails {
    99  		// convert amounts in test json to bit.Int and clear the temporary JsonValue
   100  		for i := range tx.Vout {
   101  			vout := &tx.Vout[i]
   102  			vout.ValueSat, err = parser.AmountToBigInt(vout.JsonValue)
   103  			if err != nil {
   104  				return nil, err
   105  			}
   106  			vout.JsonValue = ""
   107  		}
   108  
   109  		// get addresses parsed
   110  		err := setTxAddresses(parser, tx)
   111  		if err != nil {
   112  			return nil, err
   113  		}
   114  	}
   115  
   116  	return &v, nil
   117  }
   118  
   119  func setTxAddresses(parser bchain.BlockChainParser, tx *bchain.Tx) error {
   120  	for i := range tx.Vout {
   121  		ad, err := parser.GetAddrDescFromVout(&tx.Vout[i])
   122  		if err != nil {
   123  			return err
   124  		}
   125  		addrs := []string{}
   126  		a, s, err := parser.GetAddressesFromAddrDesc(ad)
   127  		if err == nil && s {
   128  			addrs = append(addrs, a...)
   129  		}
   130  		tx.Vout[i].ScriptPubKey.Addresses = addrs
   131  	}
   132  	return nil
   133  }
   134  
   135  func testGetBlockHash(t *testing.T, h *TestHandler) {
   136  	hash, err := h.Chain.GetBlockHash(h.TestData.BlockHeight)
   137  	if err != nil {
   138  		t.Error(err)
   139  		return
   140  	}
   141  
   142  	if hash != h.TestData.BlockHash {
   143  		t.Errorf("GetBlockHash() got %q, want %q", hash, h.TestData.BlockHash)
   144  	}
   145  }
   146  
   147  func testGetBlock(t *testing.T, h *TestHandler) {
   148  	blk, err := h.Chain.GetBlock(h.TestData.BlockHash, 0)
   149  	if err != nil {
   150  		t.Error(err)
   151  		return
   152  	}
   153  
   154  	if len(blk.Txs) != len(h.TestData.BlockTxs) {
   155  		t.Errorf("GetBlock() number of transactions: got %d, want %d", len(blk.Txs), len(h.TestData.BlockTxs))
   156  	}
   157  
   158  	for ti, tx := range blk.Txs {
   159  		if tx.Txid != h.TestData.BlockTxs[ti] {
   160  			t.Errorf("GetBlock() transaction %d: got %s, want %s", ti, tx.Txid, h.TestData.BlockTxs[ti])
   161  		}
   162  	}
   163  }
   164  
   165  func testGetTransaction(t *testing.T, h *TestHandler) {
   166  	for txid, want := range h.TestData.TxDetails {
   167  		got, err := h.Chain.GetTransaction(txid)
   168  		if err != nil {
   169  			t.Error(err)
   170  			return
   171  		}
   172  		// Confirmations is variable field, we just check if is set and reset it
   173  		if got.Confirmations <= 0 {
   174  			t.Errorf("GetTransaction() got struct with invalid Confirmations field")
   175  			continue
   176  		}
   177  		got.Confirmations = 0
   178  		// CoinSpecificData are not specified in the fixtures
   179  		got.CoinSpecificData = nil
   180  
   181  		normalizeAddresses(want, h.Chain.GetChainParser())
   182  		normalizeAddresses(got, h.Chain.GetChainParser())
   183  
   184  		if !reflect.DeepEqual(got, want) {
   185  			t.Errorf("GetTransaction() got %+#v, want %+#v", got, want)
   186  		}
   187  	}
   188  }
   189  
   190  func testGetTransactionForMempool(t *testing.T, h *TestHandler) {
   191  	for txid, want := range h.TestData.TxDetails {
   192  		// reset fields that are not parsed by BlockChainParser
   193  		want.Confirmations, want.Blocktime, want.Time, want.CoinSpecificData = 0, 0, 0, nil
   194  
   195  		got, err := h.Chain.GetTransactionForMempool(txid)
   196  		if err != nil {
   197  			t.Fatal(err)
   198  		}
   199  
   200  		normalizeAddresses(want, h.Chain.GetChainParser())
   201  		normalizeAddresses(got, h.Chain.GetChainParser())
   202  
   203  		// transactions parsed from JSON may contain additional data
   204  		got.Confirmations, got.Blocktime, got.Time, got.CoinSpecificData = 0, 0, 0, nil
   205  		if !reflect.DeepEqual(got, want) {
   206  			t.Errorf("GetTransactionForMempool() got %+#v, want %+#v", got, want)
   207  		}
   208  	}
   209  }
   210  
   211  // empty slice can be either []slice{} or nil; reflect.DeepEqual treats them as different value
   212  // remove checksums from ethereum addresses
   213  func normalizeAddresses(tx *bchain.Tx, parser bchain.BlockChainParser) {
   214  	for i := range tx.Vin {
   215  		if len(tx.Vin[i].Addresses) == 0 {
   216  			tx.Vin[i].Addresses = nil
   217  		} else {
   218  			if parser.GetChainType() == bchain.ChainEthereumType {
   219  				for j := range tx.Vin[i].Addresses {
   220  					tx.Vin[i].Addresses[j] = strings.ToLower(tx.Vin[i].Addresses[j])
   221  				}
   222  			}
   223  		}
   224  	}
   225  	for i := range tx.Vout {
   226  		if len(tx.Vout[i].ScriptPubKey.Addresses) == 0 {
   227  			tx.Vout[i].ScriptPubKey.Addresses = nil
   228  		} else {
   229  			if parser.GetChainType() == bchain.ChainEthereumType {
   230  				for j := range tx.Vout[i].ScriptPubKey.Addresses {
   231  					tx.Vout[i].ScriptPubKey.Addresses[j] = strings.ToLower(tx.Vout[i].ScriptPubKey.Addresses[j])
   232  				}
   233  			}
   234  		}
   235  	}
   236  }
   237  
   238  func testMempoolSync(t *testing.T, h *TestHandler) {
   239  	for i := 0; i < 3; i++ {
   240  		txs := getMempool(t, h)
   241  
   242  		n, err := h.Mempool.Resync()
   243  		if err != nil {
   244  			t.Fatal(err)
   245  		}
   246  		if n == 0 {
   247  			// no transactions to test
   248  			continue
   249  		}
   250  
   251  		txs = intersect(txs, getMempool(t, h))
   252  		if len(txs) == 0 {
   253  			// no transactions to test
   254  			continue
   255  		}
   256  
   257  		txid2addrs := getTxid2addrs(t, h, txs)
   258  		if len(txid2addrs) == 0 {
   259  			t.Skip("Skipping test, no addresses in mempool")
   260  		}
   261  
   262  		for txid, addrs := range txid2addrs {
   263  			for _, a := range addrs {
   264  				got, err := h.Mempool.GetTransactions(a)
   265  				if err != nil {
   266  					t.Fatalf("address %q: %s", a, err)
   267  				}
   268  				if !containsTx(got, txid) {
   269  					t.Errorf("ResyncMempool() - for address %s, transaction %s wasn't found in mempool", a, txid)
   270  					return
   271  				}
   272  			}
   273  		}
   274  
   275  		// done
   276  		return
   277  	}
   278  	t.Skip("Skipping test, all attempts to sync mempool failed due to network state changes")
   279  }
   280  
   281  func testEstimateSmartFee(t *testing.T, h *TestHandler) {
   282  	for _, blocks := range []int{1, 2, 3, 5, 10} {
   283  		fee, err := h.Chain.EstimateSmartFee(blocks, true)
   284  		if err != nil {
   285  			t.Error(err)
   286  		}
   287  		if fee.Sign() == -1 {
   288  			sf := h.Chain.GetChainParser().AmountToDecimalString(&fee)
   289  			if sf != "-1" {
   290  				t.Errorf("EstimateSmartFee() returned unexpected fee rate: %v", sf)
   291  			}
   292  		}
   293  	}
   294  }
   295  
   296  func testEstimateFee(t *testing.T, h *TestHandler) {
   297  	for _, blocks := range []int{1, 2, 3, 5, 10} {
   298  		fee, err := h.Chain.EstimateFee(blocks)
   299  		if err != nil {
   300  			t.Error(err)
   301  		}
   302  		if fee.Sign() == -1 {
   303  			sf := h.Chain.GetChainParser().AmountToDecimalString(&fee)
   304  			if sf != "-1" {
   305  				t.Errorf("EstimateFee() returned unexpected fee rate: %v", sf)
   306  			}
   307  		}
   308  	}
   309  }
   310  
   311  func testGetBestBlockHash(t *testing.T, h *TestHandler) {
   312  	for i := 0; i < 3; i++ {
   313  		hash, err := h.Chain.GetBestBlockHash()
   314  		if err != nil {
   315  			t.Fatal(err)
   316  		}
   317  
   318  		height, err := h.Chain.GetBestBlockHeight()
   319  		if err != nil {
   320  			t.Fatal(err)
   321  		}
   322  		hh, err := h.Chain.GetBlockHash(height)
   323  		if err != nil {
   324  			t.Fatal(err)
   325  		}
   326  		if hash != hh {
   327  			time.Sleep(time.Millisecond * 100)
   328  			continue
   329  		}
   330  
   331  		// we expect no next block
   332  		_, err = h.Chain.GetBlock("", height+1)
   333  		if err != nil {
   334  			if err != bchain.ErrBlockNotFound {
   335  				t.Error(err)
   336  			}
   337  			return
   338  		}
   339  	}
   340  	t.Error("GetBestBlockHash() didn't get the best hash")
   341  }
   342  
   343  func testGetBestBlockHeight(t *testing.T, h *TestHandler) {
   344  	for i := 0; i < 3; i++ {
   345  		height, err := h.Chain.GetBestBlockHeight()
   346  		if err != nil {
   347  			t.Fatal(err)
   348  		}
   349  
   350  		// we expect no next block
   351  		_, err = h.Chain.GetBlock("", height+1)
   352  		if err != nil {
   353  			if err != bchain.ErrBlockNotFound {
   354  				t.Error(err)
   355  			}
   356  			return
   357  		}
   358  	}
   359  	t.Error("GetBestBlockHeight() didn't get the best height")
   360  }
   361  
   362  func testGetBlockHeader(t *testing.T, h *TestHandler) {
   363  	want := &bchain.BlockHeader{
   364  		Hash:   h.TestData.BlockHash,
   365  		Height: h.TestData.BlockHeight,
   366  		Time:   h.TestData.BlockTime,
   367  		Size:   h.TestData.BlockSize,
   368  	}
   369  
   370  	got, err := h.Chain.GetBlockHeader(h.TestData.BlockHash)
   371  	if err != nil {
   372  		t.Fatal(err)
   373  	}
   374  
   375  	// Confirmations is variable field, we just check if is set and reset it
   376  	if got.Confirmations <= 0 {
   377  		t.Fatalf("GetBlockHeader() got struct with invalid Confirmations field")
   378  	}
   379  	got.Confirmations = 0
   380  
   381  	got.Prev, got.Next = "", ""
   382  
   383  	if !reflect.DeepEqual(got, want) {
   384  		t.Errorf("GetBlockHeader() got=%+#v, want=%+#v", got, want)
   385  	}
   386  }
   387  
   388  func getMempool(t *testing.T, h *TestHandler) []string {
   389  	txs, err := h.Chain.GetMempoolTransactions()
   390  	if err != nil {
   391  		t.Fatal(err)
   392  	}
   393  	if len(txs) == 0 {
   394  		t.Skip("Skipping test, mempool is empty")
   395  	}
   396  
   397  	return txs
   398  }
   399  
   400  func getTxid2addrs(t *testing.T, h *TestHandler, txs []string) map[string][]string {
   401  	txid2addrs := map[string][]string{}
   402  	for i := range txs {
   403  		tx, err := h.Chain.GetTransactionForMempool(txs[i])
   404  		if err != nil {
   405  			if err == bchain.ErrTxNotFound {
   406  				continue
   407  			}
   408  			t.Fatal(err)
   409  		}
   410  		setTxAddresses(h.Chain.GetChainParser(), tx)
   411  		addrs := []string{}
   412  		for j := range tx.Vout {
   413  			for _, a := range tx.Vout[j].ScriptPubKey.Addresses {
   414  				addrs = append(addrs, a)
   415  			}
   416  		}
   417  		if len(addrs) > 0 {
   418  			txid2addrs[tx.Txid] = addrs
   419  		}
   420  	}
   421  	return txid2addrs
   422  }
   423  
   424  func intersect(a, b []string) []string {
   425  	setA := mapset.NewSet()
   426  	for _, v := range a {
   427  		setA.Add(v)
   428  	}
   429  	setB := mapset.NewSet()
   430  	for _, v := range b {
   431  		setB.Add(v)
   432  	}
   433  	inter := setA.Intersect(setB)
   434  	res := make([]string, 0, inter.Cardinality())
   435  	for v := range inter.Iter() {
   436  		res = append(res, v.(string))
   437  	}
   438  	return res
   439  }
   440  
   441  func containsTx(o []bchain.Outpoint, tx string) bool {
   442  	for i := range o {
   443  		if o[i].Txid == tx {
   444  			return true
   445  		}
   446  	}
   447  	return false
   448  }