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 }