decred.org/dcrdex@v1.0.5/client/asset/dcr/externaltx.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 dcr 5 6 import ( 7 "context" 8 "fmt" 9 "sync" 10 "time" 11 12 "decred.org/dcrdex/client/asset" 13 "github.com/decred/dcrd/chaincfg/chainhash" 14 "github.com/decred/dcrd/wire" 15 ) 16 17 type externalTx struct { 18 hash *chainhash.Hash 19 20 // blockMtx protects access to the fields below it, which 21 // are set when the tx's block is found and cleared when 22 // the previously found tx block is orphaned. 23 blockMtx sync.RWMutex 24 lastScannedBlock *chainhash.Hash 25 block *block 26 tree int8 27 outputSpenders []*outputSpenderFinder 28 } 29 30 type outputSpenderFinder struct { 31 *wire.TxOut 32 op outPoint 33 tree int8 34 35 spenderMtx sync.RWMutex 36 lastScannedBlock *chainhash.Hash 37 spenderBlock *block 38 spenderTx *wire.MsgTx 39 } 40 41 // lookupTxOutWithBlockFilters returns confirmations and spend status of the 42 // requested output. If the block containing the output is not yet known, a 43 // a block filters scan is conducted to determine if the output is mined in a 44 // block between the current best block and the block just before the provided 45 // earliestTxTime. Returns asset.CoinNotFoundError if the block containing the 46 // output is not found. 47 func (dcr *ExchangeWallet) lookupTxOutWithBlockFilters(ctx context.Context, op outPoint, pkScript []byte, earliestTxTime time.Time) (uint32, bool, error) { 48 if len(pkScript) == 0 { 49 return 0, false, fmt.Errorf("cannot perform block filters lookup without a script") 50 } 51 52 output, outputBlock, err := dcr.externalTxOutput(ctx, op, pkScript, earliestTxTime) 53 if err != nil { 54 return 0, false, err // may be asset.CoinNotFoundError 55 } 56 57 spent, err := dcr.isOutputSpent(ctx, output) 58 if err != nil { 59 return 0, false, fmt.Errorf("error checking if output %s is spent: %w", op, err) 60 } 61 62 // Get the current tip height to calculate confirmations. 63 tip, err := dcr.getBestBlock(ctx) 64 if err != nil { 65 dcr.log.Errorf("getbestblock error %v", err) 66 tip = dcr.cachedBestBlock() 67 } 68 var confs uint32 69 if tip.height >= outputBlock.height { // slight possibility that the cached tip height is behind the output's block height 70 confs = uint32(tip.height + 1 - outputBlock.height) 71 } 72 return confs, spent, nil 73 } 74 75 // externalTxOutput attempts to locate the requested tx output in a mainchain 76 // block and if found, returns the output details along with the block details. 77 func (dcr *ExchangeWallet) externalTxOutput(ctx context.Context, op outPoint, pkScript []byte, earliestTxTime time.Time) (*outputSpenderFinder, *block, error) { 78 dcr.externalTxMtx.Lock() 79 tx := dcr.externalTxCache[op.txHash] 80 if tx == nil { 81 tx = &externalTx{hash: &op.txHash} 82 dcr.externalTxCache[op.txHash] = tx // never deleted (TODO) 83 } 84 dcr.externalTxMtx.Unlock() 85 86 // Hold the tx.blockMtx lock for 2 reasons: 87 // 1) To read/write the tx.block, tx.tree and tx.outputSpenders fields. 88 // 2) To prevent duplicate tx block scans if this tx block is not already 89 // known. Holding this lock now ensures that any ongoing scan completes 90 // before we try to access the tx.block field which may prevent 91 // unnecessary rescan. 92 tx.blockMtx.Lock() 93 defer tx.blockMtx.Unlock() 94 95 // First check if the tx block is cached. 96 txBlock, err := dcr.txBlockFromCache(ctx, tx) 97 if err != nil { 98 return nil, nil, fmt.Errorf("error checking if tx %s is known to be mined: %w", tx.hash, err) 99 } 100 101 // Scan block filters to find the tx block if it is yet unknown. 102 if txBlock == nil { 103 dcr.log.Tracef("Output %s:%d NOT yet found; now searching with block filters.", op.txHash, op.vout) 104 txBlock, err = dcr.scanFiltersForTxBlock(ctx, tx, [][]byte{pkScript}, earliestTxTime) 105 if err != nil { 106 return nil, nil, fmt.Errorf("error checking if tx %s is mined: %w", tx.hash, err) 107 } 108 if txBlock == nil { 109 return nil, nil, asset.CoinNotFoundError 110 } 111 } 112 113 if len(tx.outputSpenders) <= int(op.vout) { 114 return nil, nil, fmt.Errorf("tx %s does not have an output at index %d", tx.hash, op.vout) 115 } 116 return tx.outputSpenders[op.vout], txBlock, nil 117 } 118 119 // txBlockFromCache returns the block containing this tx if it's known and 120 // still part of the mainchain. It is not an error if the block is unknown 121 // or invalidated (must check for a nil *block). 122 // The tx.blockMtx MUST be locked for writing. 123 func (dcr *ExchangeWallet) txBlockFromCache(ctx context.Context, tx *externalTx) (*block, error) { 124 if tx.block == nil { 125 return nil, nil 126 } 127 128 _, _, txBlockStillValid, err := dcr.blockHeader(ctx, tx.block.hash) 129 if err != nil { 130 return nil, err 131 } 132 133 if txBlockStillValid { // both mainchain and not disapproved 134 // dcr.log.Tracef("Cached tx %s is mined in block %d (%s).", tx.hash, tx.block.height, tx.block.hash) 135 return tx.block, nil 136 } 137 138 // Tx block was previously set but seems to have been invalidated. 139 // Clear the tx tree, outputs and block info fields that must have 140 // been previously set. 141 dcr.log.Warnf("Block %s previously found to contain tx %s "+ 142 "has been orphaned or disapproved by stakeholders.", tx.block.hash, tx.hash) 143 tx.block = nil 144 tx.tree = -1 145 tx.outputSpenders = nil 146 return nil, nil 147 } 148 149 // scanFiltersForTxBlock attempts to find the block containing the provided tx 150 // by scanning block filters from the current best block down to the block just 151 // before earliestTxTime or the block that was last scanned, if there was a 152 // previous scan. If the tx block is found, the block hash, height and the tx 153 // outputs details are cached; and the block is returned. 154 // The tx.blockMtx MUST be locked for writing. 155 func (dcr *ExchangeWallet) scanFiltersForTxBlock(ctx context.Context, tx *externalTx, txScripts [][]byte, earliestTxTime time.Time) (*block, error) { 156 // Scan block filters in reverse from the current best block to the last 157 // scanned block. If the last scanned block has been re-orged out of the 158 // mainchain, scan back to the mainchain ancestor of the lastScannedBlock. 159 var lastScannedBlock *block 160 if tx.lastScannedBlock != nil { 161 stopBlockHash, stopBlockHeight, err := dcr.mainchainAncestor(ctx, tx.lastScannedBlock) 162 if err != nil { 163 return nil, fmt.Errorf("error looking up mainchain ancestor for block %s", err) 164 } 165 tx.lastScannedBlock = stopBlockHash 166 lastScannedBlock = &block{hash: stopBlockHash, height: stopBlockHeight} 167 } 168 169 // Run cfilters scan in reverse from best block to lastScannedBlock or 170 // to block just before earliestTxTime. 171 currentTip := dcr.cachedBestBlock() 172 if lastScannedBlock == nil { 173 dcr.log.Debugf("Searching for tx %s in blocks between best block %d (%s) and the block just before %s.", 174 tx.hash, currentTip.height, currentTip.hash, earliestTxTime) 175 } else if lastScannedBlock.height < currentTip.height { 176 dcr.log.Debugf("Searching for tx %s in blocks %d (%s) to %d (%s).", tx.hash, 177 lastScannedBlock.height, lastScannedBlock.hash, currentTip.height, currentTip.hash) 178 } else { 179 if lastScannedBlock.height > currentTip.height { 180 dcr.log.Warnf("Previous cfilters look up for tx %s stopped at block %d but current tip is %d?", 181 tx.hash, lastScannedBlock.height, currentTip.height) 182 } 183 return nil, nil // no new blocks to scan 184 } 185 186 iHash := currentTip.hash 187 iHeight := currentTip.height 188 189 // Set the current tip as the last scanned block so subsequent 190 // scans cover the latest tip back to this current tip. 191 scanCompletedWithoutResults := func() (*block, error) { 192 tx.lastScannedBlock = currentTip.hash 193 dcr.log.Debugf("Tx %s NOT found in blocks %d (%s) to %d (%s).", tx.hash, 194 iHeight, iHash, currentTip.height, currentTip.hash) 195 return nil, nil 196 } 197 198 earliestTxStamp := earliestTxTime.Unix() 199 for { 200 msgTx, outputSpenders, err := dcr.findTxInBlock(ctx, *tx.hash, txScripts, iHash) 201 if err != nil { 202 return nil, err 203 } 204 205 if msgTx != nil { 206 tx.block = &block{hash: iHash, height: iHeight} 207 tx.tree = determineTxTree(msgTx) 208 tx.outputSpenders = outputSpenders 209 return tx.block, nil 210 } 211 212 // Block does not include the tx, check the previous block. 213 // Abort the search if we've scanned blocks from the tip back to the 214 // block we scanned last or the block just before earliestTxTime. 215 if iHeight == 0 { 216 return scanCompletedWithoutResults() 217 } 218 if lastScannedBlock != nil && iHeight <= lastScannedBlock.height { 219 return scanCompletedWithoutResults() 220 } 221 iBlock, err := dcr.wallet.GetBlockHeader(dcr.ctx, iHash) 222 if err != nil { 223 return nil, fmt.Errorf("getblockheader error for block %s: %w", iHash, translateRPCCancelErr(err)) 224 } 225 if iBlock.Timestamp.Unix() <= earliestTxStamp { 226 return scanCompletedWithoutResults() 227 } 228 229 iHeight-- 230 iHash = &iBlock.PrevBlock 231 continue 232 } 233 } 234 235 func (dcr *ExchangeWallet) findTxInBlock(ctx context.Context, txHash chainhash.Hash, txScripts [][]byte, blockHash *chainhash.Hash) (*wire.MsgTx, []*outputSpenderFinder, error) { 236 bingo, err := dcr.wallet.MatchAnyScript(ctx, blockHash, txScripts) 237 if err != nil { 238 return nil, nil, err 239 } 240 if !bingo { 241 return nil, nil, nil 242 } 243 244 blk, err := dcr.wallet.GetBlock(ctx, blockHash) 245 if err != nil { 246 return nil, nil, fmt.Errorf("error retrieving block %s: %w", blockHash, err) 247 } 248 249 var msgTx *wire.MsgTx 250 for _, tx := range append(blk.Transactions, blk.STransactions...) { 251 if tx.TxHash() == txHash { 252 dcr.log.Debugf("Found mined tx %s in block %s.", txHash, blk.BlockHash()) 253 msgTx = tx 254 break 255 } 256 } 257 258 if msgTx == nil { 259 dcr.log.Debugf("Block %s filters matched scripts for tx %s but does NOT contain the tx.", blk.BlockHash(), txHash) 260 return nil, nil, nil 261 } 262 263 // We have the txs in this block, check if any them spends an output 264 // from the original tx. 265 outputSpenders := make([]*outputSpenderFinder, len(msgTx.TxOut)) 266 for i, txOut := range msgTx.TxOut { 267 outputSpenders[i] = &outputSpenderFinder{ 268 TxOut: txOut, 269 op: newOutPoint(&txHash, uint32(i)), 270 tree: determineTxTree(msgTx), 271 lastScannedBlock: blockHash, 272 } 273 } 274 for _, tx := range append(blk.Transactions, blk.STransactions...) { 275 if tx.TxHash() == txHash { 276 continue // original tx, ignore 277 } 278 for _, txIn := range tx.TxIn { 279 if txIn.PreviousOutPoint.Hash == txHash { // found a spender 280 outputSpenders[txIn.PreviousOutPoint.Index].spenderBlock = &block{int64(blk.Header.Height), blockHash} 281 outputSpenders[txIn.PreviousOutPoint.Index].spenderTx = tx 282 } 283 } 284 } 285 286 return msgTx, outputSpenders, nil 287 } 288 289 func (dcr *ExchangeWallet) isOutputSpent(ctx context.Context, output *outputSpenderFinder) (bool, error) { 290 // Hold the output.spenderMtx lock for 2 reasons: 291 // 1) To read (and set) the spenderBlock field. 292 // 2) To prevent duplicate spender block scans if the spenderBlock is not 293 // already known. Holding this lock now ensures that any ongoing scan 294 // completes before we try to access the output.spenderBlock field 295 // which may prevent unnecessary rescan. 296 output.spenderMtx.Lock() 297 defer output.spenderMtx.Unlock() 298 299 // Check if this output is known to be spent in a mainchain block. 300 if output.spenderBlock != nil { 301 _, _, spenderBlockStillValid, err := dcr.blockHeader(ctx, output.spenderBlock.hash) 302 if err != nil { 303 return false, err 304 } 305 if spenderBlockStillValid { // both mainchain and not disapproved 306 // dcr.log.Debugf("Found cached information for the spender of %s.", output.op) 307 return true, nil 308 } 309 // Output was previously found to have been spent but the block 310 // containing the spending tx seems to have been invalidated. 311 dcr.log.Warnf("Block %s previously found to contain spender of output %s "+ 312 "has been orphaned or disapproved by stakeholders.", 313 output.spenderBlock.hash, output.op) 314 output.spenderBlock = nil 315 output.spenderTx = nil 316 } 317 318 // This tx output is not known to be spent as of last search (if any). 319 // Scan block filters starting from the block after the tx block or the 320 // lastScannedBlock (if there was a previous scan). Use mainchainAncestor 321 // to ensure that scanning starts from a mainchain block in the event that 322 // the lastScannedBlock have been re-orged out of the mainchain. We already 323 // checked that the txBlock is not invalidated above. 324 _, lastScannedHeight, err := dcr.mainchainAncestor(ctx, output.lastScannedBlock) 325 if err != nil { 326 return false, err 327 } 328 nextScanHeight := lastScannedHeight + 1 329 330 bestBlock := dcr.cachedBestBlock() 331 if lastScannedHeight == bestBlock.height { 332 // This is fine. No more blocks to scan 333 return false, nil 334 } 335 if lastScannedHeight > bestBlock.height { 336 // This is not fine, how did it scan a height above the best 337 // height? Log a warning, should never happen anyways. 338 dcr.log.Warnf("Attempted to look for output spender in block %d but current tip is %d!", 339 nextScanHeight, bestBlock.height) 340 // Return too, since there're obviously no more blocks to scan. 341 return false, nil 342 } 343 344 // Search for this output's spender in the blocks between startBlock and 345 // the current best block. 346 nextScanHash, err := dcr.wallet.GetBlockHash(ctx, nextScanHeight) 347 if err != nil { 348 return false, err 349 } 350 spenderTx, stopBlockHash, stopBlockHeight, err := dcr.findTxOutSpender(ctx, output.op, output.PkScript, &block{nextScanHeight, nextScanHash}) 351 if stopBlockHash != nil { // might be nil if the search never scanned a block 352 output.lastScannedBlock = stopBlockHash 353 } 354 if err != nil { 355 return false, err 356 } 357 358 // Cache relevant spender info if the spender is found. 359 if spenderTx == nil { 360 return false, nil 361 } 362 363 output.spenderBlock = &block{hash: stopBlockHash, height: stopBlockHeight} 364 output.spenderTx = spenderTx 365 return true, nil 366 } 367 368 // findTxOutSpender attempts to find and return the tx that spends the provided 369 // output by matching the provided outputPkScript against the block filters of 370 // the mainchain blocks between the provided startBlock and the current best 371 // block. 372 // If no tx is found to spend the provided output, the hash of the block that 373 // was last checked is returned along with any error that may have occurred 374 // during the search. 375 func (dcr *ExchangeWallet) findTxOutSpender(ctx context.Context, op outPoint, outputPkScript []byte, startBlock *block) (*wire.MsgTx, *chainhash.Hash, int64, error) { 376 var lastScannedHash *chainhash.Hash 377 var lastScannedHeight int64 378 379 iHeight := startBlock.height 380 iHash := startBlock.hash 381 bestBlock := dcr.cachedBestBlock() 382 for { 383 bingo, err := dcr.wallet.MatchAnyScript(ctx, iHash, [][]byte{outputPkScript}) 384 if err != nil { 385 return nil, lastScannedHash, lastScannedHeight, err 386 } 387 388 if bingo { 389 dcr.log.Debugf("Output %s is likely spent in block %d (%s). Confirming.", 390 op, iHeight, iHash) 391 blk, err := dcr.wallet.GetBlock(ctx, iHash) 392 if err != nil { 393 return nil, lastScannedHash, lastScannedHeight, fmt.Errorf("error retrieving block %s: %w", iHash, err) 394 } 395 for _, tx := range append(blk.Transactions, blk.STransactions...) { 396 if txSpendsOutput(tx, op) { 397 dcr.log.Debugf("Found spender for output %s in block %d (%s), spender tx hash %s.", 398 op, iHeight, iHash, tx.TxHash()) 399 return tx, iHash, iHeight, nil 400 } 401 } 402 dcr.log.Debugf("Output %s is NOT spent in block %d (%s).", op, iHeight, iHash) 403 } 404 405 lastScannedHeight = iHeight 406 lastScannedHash = iHash 407 408 if iHeight >= bestBlock.height { // reached the tip, stop searching 409 break 410 } 411 412 // Block does not include the output spender, check the next block. 413 iHeight++ 414 nextHash, err := dcr.wallet.GetBlockHash(ctx, iHeight) 415 if err != nil { 416 return nil, lastScannedHash, lastScannedHeight, translateRPCCancelErr(err) 417 } 418 iHash = nextHash 419 } 420 421 dcr.log.Debugf("Output %s is NOT spent in blocks %d (%s) to %d (%s).", 422 op, startBlock.height, startBlock.hash, bestBlock.height, bestBlock.hash) 423 return nil, lastScannedHash, lastScannedHeight, nil // scanned up to best block, no spender found 424 } 425 426 // txSpendsOutput returns true if the passed tx has an input that spends the 427 // specified output. 428 func txSpendsOutput(tx *wire.MsgTx, op outPoint) bool { 429 if tx.TxHash() == op.txHash { 430 return false // no need to check inputs if this tx is the same tx that pays to the specified op 431 } 432 for _, txIn := range tx.TxIn { 433 prevOut := &txIn.PreviousOutPoint 434 if prevOut.Index == op.vout && prevOut.Hash == op.txHash { 435 return true // found spender 436 } 437 } 438 return false 439 }