decred.org/dcrdex@v1.0.5/server/asset/btc/testing.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package btc
     5  
     6  import (
     7  	"context"
     8  	"encoding/hex"
     9  	"errors"
    10  	"fmt"
    11  	"testing"
    12  
    13  	dexbtc "decred.org/dcrdex/dex/networks/btc"
    14  	"decred.org/dcrdex/server/asset"
    15  	"github.com/btcsuite/btcd/chaincfg/chainhash"
    16  	"github.com/btcsuite/btcd/txscript"
    17  )
    18  
    19  // LiveP2SHStats will scan the provided Backend's node for inputs that spend
    20  // pay-to-script-hash outputs. The pubkey scripts and redeem scripts are
    21  // examined to ensure the backend understands what they are and can extract
    22  // addresses. Ideally, the stats will show no scripts which were unparseable by
    23  // the backend, but the presence of unknowns is not an error.
    24  func LiveP2SHStats(btc *Backend, t *testing.T, numToDo int) {
    25  	type scriptStats struct {
    26  		unknown      int
    27  		p2pk         int
    28  		p2pkh        int
    29  		p2wpkh       int
    30  		p2wsh        int
    31  		multisig     int
    32  		escrow       int
    33  		found        int
    34  		zeros        int
    35  		empty        int
    36  		swaps        int
    37  		emptyRedeems int
    38  		addrErr      int
    39  		nonStd       int
    40  		noSigs       int
    41  	}
    42  	var stats scriptStats
    43  	hash, err := btc.node.GetBestBlockHash()
    44  	if err != nil {
    45  		t.Fatalf("error getting best block hash: %v", err)
    46  	}
    47  	block, err := btc.node.GetBlockVerbose(hash)
    48  	if err != nil {
    49  		t.Fatalf("error getting best block verbose: %v", err)
    50  	}
    51  	unknowns := []string{}
    52  	// For each txIn, grab the previous outpoint. If the outpoint pkScript is
    53  	// p2sh or p2wsh, locate the redeem script and take some stats on how the
    54  	// redeem script parses.
    55  out:
    56  	for {
    57  		for _, txid := range block.Tx {
    58  			txHash, err := chainhash.NewHashFromStr(txid)
    59  			if err != nil {
    60  				t.Fatalf("error parsing transaction hash from %s: %v", txid, err)
    61  			}
    62  			tx, err := btc.node.GetRawTransactionVerbose(txHash)
    63  			if err != nil {
    64  				t.Fatalf("error fetching transaction %s: %v", txHash, err)
    65  			}
    66  			for vin, txIn := range tx.Vin {
    67  				txOutHash, err := chainhash.NewHashFromStr(txIn.Txid)
    68  				if err != nil {
    69  					t.Fatalf("error decoding txhash from hex %s: %v", txIn.Txid, err)
    70  				}
    71  				if *txOutHash == zeroHash {
    72  					stats.zeros++
    73  					continue
    74  				}
    75  				prevOutTx, err := btc.node.GetRawTransactionVerbose(txOutHash)
    76  				if err != nil {
    77  					t.Fatalf("error fetching previous outpoint: %v", err)
    78  				}
    79  				prevOutpoint := prevOutTx.Vout[int(txIn.Vout)]
    80  				pkScript, err := hex.DecodeString(prevOutpoint.ScriptPubKey.Hex)
    81  				if err != nil {
    82  					t.Fatalf("error decoding script from hex %s: %v", prevOutpoint.ScriptPubKey.Hex, err)
    83  				}
    84  				scriptType := dexbtc.ParseScriptType(pkScript, nil)
    85  				if scriptType.IsP2SH() {
    86  					stats.found++
    87  					if stats.found > numToDo {
    88  						break out
    89  					}
    90  					var redeemScript []byte
    91  					if scriptType.IsSegwit() {
    92  						// if it's segwit, the script is the last input witness data.
    93  						redeemHex := txIn.Witness[len(txIn.Witness)-1]
    94  						redeemScript, err = hex.DecodeString(redeemHex)
    95  						if err != nil {
    96  							t.Fatalf("error decoding redeem script from hex %s: %v", redeemHex, err)
    97  						}
    98  					} else {
    99  						// If it's non-segwit P2SH, the script is the last data push
   100  						// in the scriptSig.
   101  						scriptSig, err := hex.DecodeString(txIn.ScriptSig.Hex)
   102  						if err != nil {
   103  							t.Fatalf("error decoding redeem script from hex %s: %v", txIn.ScriptSig.Hex, err)
   104  						}
   105  						pushed, err := txscript.PushedData(scriptSig)
   106  						if err != nil {
   107  							t.Fatalf("error parsing scriptSig: %v", err)
   108  						}
   109  						if len(pushed) == 0 {
   110  							stats.empty++
   111  							continue
   112  						}
   113  						redeemScript = pushed[len(pushed)-1]
   114  					}
   115  					scriptType := dexbtc.ParseScriptType(pkScript, redeemScript)
   116  					scriptClass := txscript.GetScriptClass(redeemScript)
   117  					switch scriptClass {
   118  					case txscript.MultiSigTy:
   119  						if !scriptType.IsMultiSig() {
   120  							t.Fatalf("multi-sig script class but not parsed as multi-sig")
   121  						}
   122  						stats.multisig++
   123  					case txscript.PubKeyTy:
   124  						stats.p2pk++
   125  					case txscript.PubKeyHashTy:
   126  						stats.p2pkh++
   127  					case txscript.WitnessV0PubKeyHashTy:
   128  						stats.p2wpkh++
   129  					case txscript.WitnessV0ScriptHashTy:
   130  						stats.p2wsh++
   131  					default:
   132  						_, _, _, _, err = dexbtc.ExtractSwapDetails(redeemScript, btc.segwit, btc.chainParams)
   133  						if err == nil {
   134  							stats.swaps++
   135  							continue
   136  						}
   137  						if isEscrowScript(redeemScript) {
   138  							stats.escrow++
   139  							continue
   140  						}
   141  						if len(redeemScript) == 0 {
   142  							stats.emptyRedeems++
   143  						}
   144  						unknowns = append(unknowns, txHash.String()+":"+fmt.Sprintf("%d", vin))
   145  						stats.unknown++
   146  					}
   147  					evalScript := pkScript
   148  					if scriptType.IsP2SH() {
   149  						evalScript = redeemScript
   150  					}
   151  					scriptAddrs, nonStandard, err := dexbtc.ExtractScriptAddrs(evalScript, btc.chainParams)
   152  					if err != nil {
   153  						stats.addrErr++
   154  						continue
   155  					}
   156  					if nonStandard {
   157  						stats.nonStd++
   158  					}
   159  					if scriptAddrs.NRequired == 0 {
   160  						stats.noSigs++
   161  					}
   162  				}
   163  			}
   164  		}
   165  		prevHash, err := chainhash.NewHashFromStr(block.PreviousHash)
   166  		if err != nil {
   167  			t.Fatalf("error decoding previous block hash: %v", err)
   168  		}
   169  		block, err = btc.node.GetBlockVerbose(prevHash)
   170  		if err != nil {
   171  			t.Fatalf("error getting previous block verbose: %v", err)
   172  		}
   173  	}
   174  	t.Logf("%d P2WPKH redeem scripts", stats.p2wpkh)
   175  	t.Logf("%d P2WSH redeem scripts", stats.p2wsh)
   176  	t.Logf("%d multi-sig redeem scripts", stats.multisig)
   177  	t.Logf("%d P2PK redeem scripts", stats.p2pk)
   178  	t.Logf("%d P2PKH redeem scripts", stats.p2pkh)
   179  	t.Logf("%d unknown redeem scripts, %d of which were empty", stats.unknown, stats.emptyRedeems)
   180  	t.Logf("%d previous outpoint zero hashes (coinbase)", stats.zeros)
   181  	t.Logf("%d atomic swap contract redeem scripts", stats.swaps)
   182  	t.Logf("%d escrow scripts", stats.escrow)
   183  	t.Logf("%d error parsing addresses from script", stats.addrErr)
   184  	t.Logf("%d scripts parsed with 0 required signatures", stats.noSigs)
   185  	t.Logf("%d unexpected empty scriptSig", stats.empty)
   186  	numUnknown := len(unknowns)
   187  	if numUnknown > 0 {
   188  		numToShow := 5
   189  		if numUnknown < numToShow {
   190  			numToShow = numUnknown
   191  		}
   192  		t.Logf("showing %d of %d unknown scripts", numToShow, numUnknown)
   193  		for i, unknown := range unknowns {
   194  			if i == numToShow {
   195  				break
   196  			}
   197  			t.Logf("    %x", unknown)
   198  		}
   199  	} else {
   200  		t.Logf("no unknown script types")
   201  	}
   202  }
   203  
   204  // LiveUTXOStats will scan the provided Backend's node for transaction
   205  // outputs. The outputs are requested with GetRawTransactionVerbose, and
   206  // statistics collected regarding spendability and pubkey script types. This
   207  // test does not request via the Backend.UTXO method and is not meant to
   208  // cover that code. Instead, these tests check the backend's real-world
   209  // blockchain literacy. Ideally, the stats will show no scripts which were
   210  // unparseable by the backend, but the presence of unknowns is not an error.
   211  func LiveUTXOStats(btc *Backend, t *testing.T) {
   212  	const numToDo = 5000
   213  	hash, err := btc.node.GetBestBlockHash()
   214  	if err != nil {
   215  		t.Fatalf("error getting best block hash: %v", err)
   216  	}
   217  	block, verboseHeader, err := btc.node.getBlockWithVerboseHeader(hash)
   218  	if err != nil {
   219  		t.Fatalf("error getting best block verbose: %v", err)
   220  	}
   221  	height := verboseHeader.Height
   222  	t.Logf("Processing block %v (%d)", hash, height)
   223  	type testStats struct {
   224  		p2pkh    int
   225  		p2wpkh   int
   226  		p2pk     int
   227  		p2sh     int
   228  		p2wsh    int
   229  		zeros    int
   230  		unknown  int
   231  		found    int
   232  		checked  int
   233  		utxoErr  int
   234  		utxoVal  uint64
   235  		feeRates []uint64
   236  	}
   237  	var stats testStats
   238  	var unknowns [][]byte
   239  	var processed int
   240  out:
   241  	for {
   242  		for _, msgTx := range block.Transactions {
   243  			for vout, txOut := range msgTx.TxOut {
   244  				if txOut.Value == 0 {
   245  					stats.zeros++
   246  					continue
   247  				}
   248  				pkScript := txOut.PkScript
   249  				scriptType := dexbtc.ParseScriptType(pkScript, nil)
   250  				if scriptType == dexbtc.ScriptUnsupported {
   251  					unknowns = append(unknowns, pkScript)
   252  					stats.unknown++
   253  					continue
   254  				}
   255  				processed++
   256  				if processed >= numToDo {
   257  					break out
   258  				}
   259  				txhash := msgTx.TxHash()
   260  				if scriptType.IsP2PKH() {
   261  					stats.p2pkh++
   262  				} else if scriptType.IsP2WPKH() {
   263  					stats.p2wpkh++
   264  				} else if scriptType.IsP2SH() {
   265  					stats.p2sh++
   266  					continue // no redeem script, can't use the utxo method
   267  				} else if scriptType.IsP2WSH() {
   268  					stats.p2wsh++
   269  					continue // no redeem script, can't use the utxo method
   270  				} else if scriptType.IsP2PK() { // rare, so last
   271  					t.Logf("p2pk: txout %v:%d", txhash, vout)
   272  					stats.p2pk++
   273  				} else {
   274  					stats.unknown++
   275  					t.Logf("other unknown script type: %v", scriptType)
   276  				}
   277  				stats.checked++
   278  
   279  				utxo, err := btc.utxo(&txhash, uint32(vout), nil)
   280  				if err != nil {
   281  					if !errors.Is(err, asset.CoinNotFoundError) {
   282  						t.Log(err, txhash)
   283  						stats.utxoErr++
   284  					}
   285  					continue
   286  				}
   287  				stats.feeRates = append(stats.feeRates, utxo.FeeRate())
   288  				stats.found++
   289  				stats.utxoVal += utxo.Value()
   290  			}
   291  		}
   292  		prevHash := block.Header.PrevBlock
   293  		block, verboseHeader, err = btc.node.getBlockWithVerboseHeader(&prevHash)
   294  		if err != nil {
   295  			t.Fatalf("error getting previous block verbose: %v", err)
   296  		}
   297  		height = verboseHeader.Height
   298  		h0 := block.BlockHash()
   299  		hash = &h0
   300  		t.Logf("Processing block %v (%d)", hash, height)
   301  	}
   302  	t.Logf("%d P2PKH scripts", stats.p2pkh)
   303  	t.Logf("%d P2WPKH scripts", stats.p2wpkh)
   304  	t.Logf("%d P2PK scripts", stats.p2pk)
   305  	t.Logf("%d P2SH scripts", stats.p2sh)
   306  	t.Logf("%d P2WSH scripts", stats.p2wsh)
   307  	t.Logf("%d zero-valued outputs", stats.zeros)
   308  	t.Logf("%d P2(W)PK(H) UTXOs found of %d checked, %.1f%%", stats.found, stats.checked, float64(stats.found)/float64(stats.checked)*100)
   309  	t.Logf("total unspent value counted: %.2f", float64(stats.utxoVal)/1e8)
   310  	t.Logf("%d P2PK(H) UTXO retrieval errors", stats.utxoErr)
   311  	numUnknown := len(unknowns)
   312  	if numUnknown > 0 {
   313  		numToShow := 5
   314  		if numUnknown < numToShow {
   315  			numToShow = numUnknown
   316  		}
   317  		t.Logf("showing %d of %d unknown scripts", numToShow, numUnknown)
   318  		for i, unknown := range unknowns {
   319  			if i == numToShow {
   320  				break
   321  			}
   322  			t.Logf("    %s", hex.EncodeToString(unknown))
   323  		}
   324  	} else {
   325  		t.Logf("no unknown script types")
   326  	}
   327  	// Fees
   328  	feeCount := len(stats.feeRates)
   329  	if feeCount > 0 {
   330  		var feeSum uint64
   331  		for _, r := range stats.feeRates {
   332  			feeSum += r
   333  		}
   334  		t.Logf("%d fees, avg rate %d", feeCount, feeSum/uint64(feeCount))
   335  	}
   336  }
   337  
   338  // LiveFeeRates scans a mapping of txid -> fee rate checking that the backend
   339  // returns the expected fee rate.
   340  func LiveFeeRates(btc *Backend, t *testing.T, standards map[string]uint64) {
   341  	for txid, expRate := range standards {
   342  		txHash, err := chainhash.NewHashFromStr(txid)
   343  		if err != nil {
   344  			t.Fatalf("error parsing transaction hash from %s: %v", txid, err)
   345  		}
   346  		verboseTx, err := btc.node.GetRawTransactionVerbose(txHash)
   347  		if err != nil {
   348  			t.Fatalf("error getting raw transaction: %v", err)
   349  		}
   350  		tx, err := btc.transaction(txHash, verboseTx)
   351  		if err != nil {
   352  			t.Fatalf("error retrieving transaction %s", txid)
   353  		}
   354  		if tx.feeRate != expRate {
   355  			t.Fatalf("unexpected fee rate for %s. expected %d, got %d", txid, expRate, tx.feeRate)
   356  		}
   357  	}
   358  }
   359  
   360  // This is an unsupported type of script, but one of the few that is fairly
   361  // common.
   362  func isEscrowScript(script []byte) bool {
   363  	if len(script) != 77 {
   364  		return false
   365  	}
   366  	if script[0] == txscript.OP_IF &&
   367  		script[1] == txscript.OP_DATA_33 &&
   368  		script[35] == txscript.OP_ELSE &&
   369  		script[36] == txscript.OP_DATA_2 &&
   370  		script[39] == txscript.OP_CHECKSEQUENCEVERIFY &&
   371  		script[40] == txscript.OP_DROP &&
   372  		script[41] == txscript.OP_DATA_33 &&
   373  		script[75] == txscript.OP_ENDIF &&
   374  		script[76] == txscript.OP_CHECKSIG {
   375  
   376  		return true
   377  	}
   378  	return false
   379  }
   380  
   381  func TestMedianFees(btc *Backend, t *testing.T) {
   382  	// The easy way.
   383  	medianFees, err := btc.node.medianFeeRate()
   384  	if err != nil {
   385  		t.Fatalf("medianFeeRate error: %v", err)
   386  	}
   387  	fmt.Printf("medianFeeRate: %v \n", medianFees)
   388  }
   389  
   390  func TestMedianFeesTheHardWay(btc *Backend, t *testing.T) {
   391  	// The hard way.
   392  	medianFees, err := btc.node.medianFeesTheHardWay(context.Background())
   393  	if err != nil {
   394  		t.Fatalf("medianFeesTheHardWay error: %v", err)
   395  	}
   396  
   397  	fmt.Printf("medianFeesTheHardWay: %v \n", medianFees)
   398  }