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 }