decred.org/dcrdex@v1.0.5/server/asset/dcr/live_test.go (about)

     1  //go:build dcrlive
     2  
     3  // Since at least one live test runs for an hour, you should run live tests
     4  // individually using the -run flag. All of these tests will only run with the
     5  // 'dcrlive' build tag, specified with the -tags flag.
     6  //
     7  // go test -v -tags dcrlive -run LiveUTXO
     8  // -----------------------------------
     9  // This command will check the UTXO paths by iterating backwards through
    10  // the transactions in the mainchain, starting with mempool, and requesting
    11  // all found outputs through the Backend.utxo method. All utxos must be
    12  // found or found to be spent.
    13  //
    14  // go test -v -tags dcrlive -run CacheAdvantage
    15  // -----------------------------------------
    16  // Check the difference between using the block cache and requesting via RPC.
    17  //
    18  // go test -v -tags dcrlive -run BlockMonitor -timeout 61m
    19  // ------------------------------------------
    20  // Monitor the block chain for a while and make sure that the block cache is
    21  // updating appropriately.
    22  
    23  package dcr
    24  
    25  import (
    26  	"context"
    27  	"encoding/json"
    28  	"fmt"
    29  	"os"
    30  	"sync"
    31  	"testing"
    32  	"time"
    33  
    34  	"decred.org/dcrdex/dex"
    35  	dexdcr "decred.org/dcrdex/dex/networks/dcr"
    36  	"decred.org/dcrdex/server/asset"
    37  	"github.com/decred/dcrd/chaincfg/chainhash"
    38  	chainjson "github.com/decred/dcrd/rpc/jsonrpc/types/v4"
    39  	"github.com/decred/dcrd/wire"
    40  )
    41  
    42  var (
    43  	dcr        *Backend
    44  	ctx        context.Context
    45  	testLogger dex.Logger
    46  )
    47  
    48  func TestMain(m *testing.M) {
    49  	// Wrap everything for defers.
    50  	doIt := func() int {
    51  		logger := dex.StdOutLogger("DCRTEST", dex.LevelTrace)
    52  
    53  		var cancel context.CancelFunc
    54  		ctx, cancel = context.WithCancel(context.Background())
    55  		var wg *sync.WaitGroup
    56  		defer func() {
    57  			logger.Infof("Shutting down...")
    58  			cancel()
    59  			if wg != nil {
    60  				wg.Wait()
    61  			}
    62  			logger.Infof("done.")
    63  		}()
    64  
    65  		var err error
    66  		dcr, err = NewBackend(&asset.BackendConfig{
    67  			AssetID: BipID,
    68  			Logger:  logger,
    69  			Net:     dex.Mainnet,
    70  		})
    71  		if err != nil {
    72  			fmt.Printf("NewBackend error: %v\n", err)
    73  			return 1
    74  		}
    75  
    76  		wg, err = dcr.Connect(ctx)
    77  		if err != nil {
    78  			fmt.Printf("Connect failed: %v", err)
    79  			return 1
    80  		}
    81  
    82  		return m.Run()
    83  	}
    84  
    85  	os.Exit(doIt())
    86  }
    87  
    88  // TestLiveUTXO will iterate the blockchain backwards, starting with mempool,
    89  // checking that UTXOs are behaving as expected along the way. Stats will be
    90  // collected on the types of scripts found.
    91  func TestLiveUTXO(t *testing.T) {
    92  	var bestHash *chainhash.Hash
    93  	var mempool []*wire.MsgTx
    94  	var txs []*wire.MsgTx
    95  	type testStats struct {
    96  		p2pkh          int
    97  		p2pk           int
    98  		sp2pkh         int
    99  		p2pkSchnorr    int
   100  		p2pkEdwards    int
   101  		p2pkhSchnorr   int
   102  		p2pkhEdwards   int
   103  		p2sh           int
   104  		sp2sh          int
   105  		immatureBefore int
   106  		immatureAfter  int
   107  		utxoVal        uint64
   108  		feeRates       []uint64
   109  	}
   110  	stats := new(testStats)
   111  	var currentHeight, tipHeight int64
   112  	maturity := uint32(chainParams.CoinbaseMaturity)
   113  	numBlocks := 512
   114  
   115  	ctx, cancel := context.WithCancel(context.Background())
   116  	defer cancel()
   117  
   118  	// Check if the best hash is still bestHash.
   119  	blockChanged := func() bool {
   120  		h, _, _ := dcr.client.GetBestBlock(ctx)
   121  		return *bestHash != *h
   122  	}
   123  
   124  	// Get the MsgTxs for the list of hashes.
   125  	getMsgTxs := func(hashes []*chainhash.Hash) []*wire.MsgTx {
   126  		outTxs := make([]*wire.MsgTx, 0)
   127  		for _, h := range hashes {
   128  			newTx, err := dcr.client.GetRawTransaction(ctx, h)
   129  			if err != nil {
   130  				t.Fatalf("error retrieving MsgTx: %v", err)
   131  				outTxs = append(outTxs, newTx.MsgTx())
   132  			}
   133  		}
   134  		return outTxs
   135  	}
   136  
   137  	// A function to look through the current known transactions to check if an
   138  	// output is spent.
   139  	txOutInList := func(vout uint32, hash chainhash.Hash, txList []*wire.MsgTx) bool {
   140  		for _, t := range txList {
   141  			for _, in := range t.TxIn {
   142  				if in.PreviousOutPoint.Hash == hash && in.PreviousOutPoint.Index == vout {
   143  					return true
   144  				}
   145  			}
   146  		}
   147  		return false
   148  	}
   149  
   150  	// Check if the transaction is spent in a known block.
   151  	txOutIsSpent := func(hash chainhash.Hash, vout uint32) bool {
   152  		return txOutInList(vout, hash, txs) || txOutInList(vout, hash, mempool)
   153  	}
   154  
   155  	refreshMempool := func() {
   156  		mpHashes, err := dcr.client.GetRawMempool(ctx, "all")
   157  		if err != nil {
   158  			t.Fatalf("getrawmempool error: %v", err)
   159  		}
   160  		mempool = getMsgTxs(mpHashes)
   161  	}
   162  
   163  	// checkTransactions checks that the UTXO for all outputs is what is expected.
   164  	checkTransactions := func(expectedConfs uint32, txSet []*wire.MsgTx) error {
   165  		txs = append(txs, txSet...)
   166  		for _, msgTx := range txSet {
   167  			txHash := msgTx.CachedTxHash()
   168  			fee := false
   169  			for vout, out := range msgTx.TxOut {
   170  				if out.Value == 0 {
   171  					continue
   172  				}
   173  				if out.Version != 0 {
   174  					continue
   175  				}
   176  				scriptType := dexdcr.ParseScriptType(out.Version, out.PkScript)
   177  				// We can't do P2SH during live testing, because we don't have the
   178  				// scripts. Just count them for now.
   179  				if scriptType.IsP2SH() {
   180  					switch {
   181  					case scriptType.IsStake():
   182  						stats.sp2sh++
   183  					default:
   184  						stats.p2sh++
   185  					}
   186  					continue
   187  				} else if scriptType.IsP2PKH() {
   188  					switch {
   189  					case scriptType&dexdcr.ScriptSigEdwards != 0:
   190  						stats.p2pkhEdwards++
   191  					case scriptType&dexdcr.ScriptSigSchnorr != 0:
   192  						stats.p2pkhSchnorr++
   193  					case scriptType.IsStake():
   194  						stats.sp2pkh++
   195  					default:
   196  						stats.p2pkh++
   197  					}
   198  				} else if scriptType.IsP2PK() {
   199  					switch {
   200  					case scriptType&dexdcr.ScriptSigEdwards != 0:
   201  						stats.p2pkEdwards++
   202  					case scriptType&dexdcr.ScriptSigSchnorr != 0:
   203  						stats.p2pkSchnorr++
   204  					default:
   205  						stats.p2pk++
   206  					}
   207  				}
   208  				// Check if its an acceptable script type.
   209  				scriptTypeOK := scriptType != dexdcr.ScriptUnsupported
   210  				// Now try to get the UTXO with the Backend
   211  				utxo, err := dcr.utxo(ctx, txHash, uint32(vout), nil)
   212  				// Can't do stakebase or coinbase.
   213  				// ToDo: Use a custom error and check it.
   214  				if err == immatureTransactionError {
   215  					// just count these for now.
   216  					confs := tipHeight - currentHeight + 1
   217  					if confs < int64(maturity) {
   218  						stats.immatureBefore++
   219  					} else {
   220  						stats.immatureAfter++
   221  					}
   222  					continue
   223  				}
   224  				// There are 4 possibilities
   225  				// 1. script is of acceptable type and utxo was found, in which case there
   226  				//    should be zero confirmations (unless a new block snuck in).
   227  				// 2. script is of acceptable type and utxo was not found. Error
   228  				//    unless output is spent in mempool.
   229  				// 3. script is of unacceptable type, and is found. Error.
   230  				// 4. script is of unacceptable type, and is not found. Should receive an
   231  				//    dex.UnsupportedScriptError.
   232  				switch true {
   233  				case scriptTypeOK && err == nil:
   234  					if !fee {
   235  						fee = true
   236  						stats.feeRates = append(stats.feeRates, utxo.FeeRate())
   237  					}
   238  					// Just check for no error on Confirmations.
   239  					confs, err := utxo.Confirmations(ctx)
   240  					if err != nil {
   241  						return fmt.Errorf("error getting confirmations for mempool tx output: %w", err)
   242  					}
   243  					if confs != int64(expectedConfs) {
   244  						return fmt.Errorf("expected %d confirmations, found %d for %s:%d", expectedConfs, confs, txHash, vout)
   245  					}
   246  					stats.utxoVal += utxo.Value()
   247  					break
   248  				case scriptTypeOK && err != nil:
   249  					// This is only okay if output is being spent by another transaction.
   250  					// Since we are iterating backwards starting with mempool, we would
   251  					// already know the spending transaction and have it stored.
   252  					if !txOutIsSpent(*txHash, uint32(vout)) {
   253  						return fmt.Errorf("unexpected UTXO error: %w", err)
   254  					}
   255  					break
   256  				case !scriptTypeOK && err == nil:
   257  					return fmt.Errorf("received UTXO for unacceptable script type")
   258  				default: // !scriptTypeOK && err != nil
   259  					// this is normal. Do nothing.
   260  				} // end switch
   261  			} // end tx check
   262  		} // end tx loop
   263  		return nil
   264  	}
   265  
   266  	// Wraps checkTransactions with some additional checks.
   267  	scanUtxos := func(confs uint32, txs []*wire.MsgTx) bool {
   268  		if err := checkTransactions(confs, txs); err != nil {
   269  			if blockChanged() {
   270  				return true
   271  			}
   272  			// Try with fresh mempool before failing.
   273  			refreshMempool()
   274  			if err = checkTransactions(confs, txs); err != nil {
   275  				t.Fatalf("failed transaction check for %d confs: %v", confs, err)
   276  			}
   277  		}
   278  		return false
   279  	}
   280  
   281  	var confs uint32
   282  	var prevHash *chainhash.Hash
   283  	// Check the next (actually the previous) block. The returned bool indicates
   284  	// that a block change was detected so the test should be run again.
   285  	checkNextBlock := func() bool {
   286  		block, err := dcr.client.GetBlock(ctx, prevHash)
   287  		currentHeight = int64(block.Header.Height)
   288  		if err != nil {
   289  			t.Fatalf("error retrieving previous block (%s): %v", prevHash, err)
   290  		}
   291  		blockTxs := append([]*wire.MsgTx(nil), block.Transactions...)
   292  		if scanUtxos(confs, append(blockTxs, block.STransactions...)) {
   293  			return true
   294  		}
   295  		prevHash = &block.Header.PrevBlock
   296  		confs++
   297  		return false
   298  	}
   299  
   300  	for {
   301  		// The loop can continue until a test completes without any block changes.
   302  		var err error
   303  		bestHash, currentHeight, err = dcr.client.GetBestBlock(ctx)
   304  		prevHash = bestHash
   305  		tipHeight = currentHeight
   306  		if err != nil {
   307  			t.Fatalf("error retrieving best block: %v", err)
   308  		}
   309  		refreshMempool()
   310  		if scanUtxos(0, mempool) {
   311  			continue
   312  		}
   313  
   314  		// Scan 10 more blocks.
   315  		confs = 1
   316  		startOver := false
   317  		for i := 0; i < numBlocks; i++ {
   318  			if checkNextBlock() {
   319  				startOver = true
   320  				break
   321  			}
   322  		}
   323  		if !startOver {
   324  			break
   325  		}
   326  	}
   327  	t.Logf("%d P2PKH scripts", stats.p2pkh)
   328  	t.Logf("%d stake P2PKH scripts", stats.sp2pkh)
   329  	t.Logf("%d Schnorr P2PKH scripts", stats.p2pkhSchnorr)
   330  	t.Logf("%d Edwards P2PKH scripts", stats.p2pkhEdwards)
   331  	t.Logf("%d P2SH scripts", stats.p2sh)
   332  	t.Logf("%d P2PK scripts", stats.p2pk)
   333  	t.Logf("%d Schnorr P2PK scripts", stats.p2pkSchnorr)
   334  	t.Logf("%d Edwards P2PK scripts", stats.p2pkEdwards)
   335  	t.Logf("%d stake P2SH scripts", stats.sp2sh)
   336  	t.Logf("%d immature transactions in the last %d blocks", stats.immatureBefore, maturity)
   337  	t.Logf("%d immature transactions before %d blocks ago", stats.immatureAfter, maturity)
   338  	t.Logf("total unspent value counted: %.2f DCR", float64(stats.utxoVal)/1e8)
   339  	feeCount := len(stats.feeRates)
   340  	if feeCount > 0 {
   341  		var feeSum uint64
   342  		for _, r := range stats.feeRates {
   343  			feeSum += r
   344  		}
   345  		t.Logf("%d fees, avg rate %d", feeCount, feeSum/uint64(feeCount))
   346  	}
   347  }
   348  
   349  // TestCacheAdvantage compares the speed of requesting blocks from the RPC vs.
   350  // using the cache to provide justification the added complexity.
   351  func TestCacheAdvantage(t *testing.T) {
   352  	client := dcr.client
   353  	nextHash, _, err := client.GetBestBlock(ctx)
   354  	if err != nil {
   355  		t.Fatalf("error retrieving best block info")
   356  	}
   357  	numBlocks := 10000
   358  	blocks := make([]*chainjson.GetBlockVerboseResult, 0, numBlocks)
   359  	start := time.Now()
   360  	// Download the blocks over RPC, recording the duration.
   361  	for i := 0; i < numBlocks; i++ {
   362  		block, err := client.GetBlockVerbose(ctx, nextHash, false)
   363  		if err != nil {
   364  			t.Fatalf("error retrieving %d-th block from the tip: %v", i, err)
   365  		}
   366  		blocks = append(blocks, block)
   367  		nextHash, err = chainhash.NewHashFromStr(block.PreviousHash)
   368  		if err != nil {
   369  			t.Fatalf("error decoding block id %s: %v", block.PreviousHash, err)
   370  		}
   371  	}
   372  	t.Logf("%d blocks fetched via RPC in %.3f ms", numBlocks, float64(time.Since(start).Nanoseconds())/1e6)
   373  	// Now go back trough the blocks, summing the encoded size and building a
   374  	// slice of hashes.
   375  	cache := newBlockCache(testLogger)
   376  	byteCount := 0
   377  	hashes := make([]*chainhash.Hash, 0, numBlocks)
   378  	for _, block := range blocks {
   379  		jsonBlock, err := json.Marshal(block)
   380  		if err != nil {
   381  			t.Fatalf("json encode error for block %s", block.Hash)
   382  		}
   383  		byteCount += len(jsonBlock)
   384  		cache.add(block)
   385  		hash, err := chainhash.NewHashFromStr(block.Hash)
   386  		if err != nil {
   387  			t.Fatalf("error decoding hash from %s", block.Hash)
   388  		}
   389  		hashes = append(hashes, hash)
   390  	}
   391  	t.Logf("%.1f MB of RPC bandwidth", float32(byteCount)/1e6)
   392  	start = time.Now()
   393  	// See how long it takes to retrieve the same block info from the cache.
   394  	for _, hash := range hashes {
   395  		b, found := cache.block(hash)
   396  		if !found {
   397  			t.Fatalf("blockCache test failed to find %s", hash)
   398  		}
   399  		_ = b
   400  	}
   401  	t.Logf("%d cached blocks retrieved in %.3f ms", numBlocks, float64(time.Since(start).Nanoseconds())/1e6)
   402  }
   403  
   404  // TestBlockMonitor is a live test that connects to dcrd and listens for block
   405  // updates, checking the state of the cache along the way.
   406  func TestBlockMonitor(t *testing.T) {
   407  	testDuration := 60 * time.Minute
   408  	fmt.Printf("Starting BlockMonitor test. Test will last for %d minutes\n", int(testDuration.Minutes()))
   409  	blockChan := dcr.BlockChannel(5)
   410  	expire := time.NewTimer(testDuration).C
   411  	lastHeight := dcr.blockCache.tipHeight()
   412  out:
   413  	for {
   414  		select {
   415  		case update := <-blockChan:
   416  			if update.Err != nil {
   417  				t.Fatalf("error encountered while monitoring blocks: %v", update.Err)
   418  			}
   419  			tipHeight := dcr.blockCache.tipHeight()
   420  			if update.Reorg {
   421  				fmt.Printf("block received at height %d causes a %d block reorg\n", tipHeight, lastHeight-tipHeight+1)
   422  			} else {
   423  				fmt.Printf("block received for height %d\n", tipHeight)
   424  			}
   425  			lastHeight = tipHeight
   426  			_, err := dcr.getMainchainDcrBlock(ctx, tipHeight)
   427  			if err != nil {
   428  				t.Fatalf("error getting newly connected block at height %d", tipHeight)
   429  			}
   430  		case <-ctx.Done():
   431  			break out
   432  		case <-expire:
   433  			break out
   434  		}
   435  	}
   436  }