decred.org/dcrdex@v1.0.5/client/asset/btc/spv_test.go (about) 1 //go:build !spvlive && !harness 2 3 // This code is available on the terms of the project LICENSE.md file, 4 // also available online at https://blueoakcouncil.org/license/1.0.0. 5 6 package btc 7 8 import ( 9 "context" 10 "errors" 11 "fmt" 12 "testing" 13 "time" 14 15 "decred.org/dcrdex/client/asset" 16 "decred.org/dcrdex/dex" 17 "decred.org/dcrdex/dex/encode" 18 "github.com/btcsuite/btcd/btcec/v2" 19 "github.com/btcsuite/btcd/btcjson" 20 "github.com/btcsuite/btcd/btcutil" 21 "github.com/btcsuite/btcd/btcutil/gcs" 22 "github.com/btcsuite/btcd/btcutil/gcs/builder" 23 "github.com/btcsuite/btcd/btcutil/psbt" 24 "github.com/btcsuite/btcd/chaincfg" 25 "github.com/btcsuite/btcd/chaincfg/chainhash" 26 "github.com/btcsuite/btcd/peer" 27 "github.com/btcsuite/btcd/txscript" 28 "github.com/btcsuite/btcd/wire" 29 "github.com/btcsuite/btcwallet/chain" 30 "github.com/btcsuite/btcwallet/waddrmgr" 31 "github.com/btcsuite/btcwallet/wallet" 32 "github.com/btcsuite/btcwallet/walletdb" 33 "github.com/btcsuite/btcwallet/wtxmgr" 34 "github.com/lightninglabs/neutrino" 35 "github.com/lightninglabs/neutrino/headerfs" 36 ) 37 38 type tBtcWallet struct { 39 *testData 40 } 41 42 func (c *tBtcWallet) ListSinceBlock(start, end, syncHeight int32) ([]btcjson.ListTransactionsResult, error) { 43 return nil, nil 44 } 45 46 func (c *tBtcWallet) PublishTransaction(tx *wire.MsgTx, label string) error { 47 c.sentRawTx = tx 48 if c.sendErr != nil { 49 return c.sendErr 50 } 51 if c.sendToAddressErr != nil { 52 return c.sendToAddressErr 53 } 54 if c.badSendHash != nil { 55 return errors.New("bad hash") 56 } 57 return c.sendErr 58 } 59 60 func (c *tBtcWallet) CalculateAccountBalances(account uint32, confirms int32) (wallet.Balances, error) { 61 if c.getBalancesErr != nil { 62 return wallet.Balances{}, c.getBalancesErr 63 } 64 bal := &c.getBalances.Mine 65 mustAmount := func(v float64) btcutil.Amount { 66 amt, _ := btcutil.NewAmount(v) 67 return amt 68 } 69 return wallet.Balances{ 70 Total: mustAmount(bal.Trusted + bal.Untrusted), 71 Spendable: mustAmount(bal.Trusted + bal.Untrusted), 72 ImmatureReward: mustAmount(bal.Immature), 73 }, nil 74 } 75 76 func (c *tBtcWallet) ListUnspent(minconf, maxconf int32, acctName string) ([]*btcjson.ListUnspentResult, error) { 77 if c.listUnspentErr != nil { 78 return nil, c.listUnspentErr 79 } 80 unspents := make([]*btcjson.ListUnspentResult, 0, len(c.listUnspent)) 81 for _, u := range c.listUnspent { 82 unspents = append(unspents, &btcjson.ListUnspentResult{ 83 TxID: u.TxID, 84 Vout: u.Vout, 85 Address: u.Address, 86 // Account: , 87 ScriptPubKey: u.ScriptPubKey.String(), 88 RedeemScript: u.RedeemScript.String(), 89 Amount: u.Amount, 90 Confirmations: int64(u.Confirmations), 91 Spendable: u.Spendable, 92 }) 93 } 94 95 return unspents, nil 96 } 97 98 func (c *tBtcWallet) FetchInputInfo(prevOut *wire.OutPoint) (*wire.MsgTx, *wire.TxOut, *psbt.Bip32Derivation, int64, error) { 99 return c.fetchInputInfoTx, nil, nil, 0, nil 100 } 101 102 func (c *tBtcWallet) ResetLockedOutpoints() {} 103 104 func (c *tBtcWallet) LockOutpoint(op wire.OutPoint) { 105 if c.lockedCoins != nil { 106 // check if already locked 107 for _, l := range c.lockedCoins { 108 if l.TxID == op.Hash.String() && l.Vout == op.Index { 109 return 110 } 111 } 112 113 c.lockedCoins = append(c.lockedCoins, &RPCOutpoint{ 114 TxID: op.Hash.String(), 115 Vout: op.Index, 116 }) 117 return 118 } 119 120 c.lockedCoins = []*RPCOutpoint{{ 121 TxID: op.Hash.String(), 122 Vout: op.Index, 123 }} 124 } 125 126 func (c *tBtcWallet) UnlockOutpoint(op wire.OutPoint) {} 127 128 func (c *tBtcWallet) LockedOutpoints() []btcjson.TransactionInput { 129 unspents := make([]btcjson.TransactionInput, 0) 130 for _, u := range c.listLockUnspent { 131 unspents = append(unspents, btcjson.TransactionInput{ 132 Txid: u.TxID, 133 Vout: u.Vout, 134 }) 135 } 136 137 return unspents 138 } 139 140 func (c *tBtcWallet) NewChangeAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) { 141 if c.changeAddrErr != nil { 142 return nil, c.changeAddrErr 143 } 144 return btcutil.DecodeAddress(c.changeAddr, &chaincfg.MainNetParams) 145 } 146 147 func (c *tBtcWallet) NewAddress(account uint32, scope waddrmgr.KeyScope) (btcutil.Address, error) { 148 if c.newAddressErr != nil { 149 return nil, c.newAddressErr 150 } 151 return btcutil.DecodeAddress(c.newAddress, &chaincfg.MainNetParams) 152 } 153 154 func (c *tBtcWallet) SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType, additionalPrevScriptsadditionalPrevScripts map[wire.OutPoint][]byte, 155 additionalKeysByAddress map[string]*btcutil.WIF, p2shRedeemScriptsByAddress map[string][]byte) ([]wallet.SignatureError, error) { 156 157 if c.signTxErr != nil { 158 return nil, c.signTxErr 159 } 160 c.signFunc(tx) 161 if c.sigIncomplete { 162 return []wallet.SignatureError{{Error: errors.New("tBtcWallet SignTransaction error")}}, nil 163 } 164 return nil, nil 165 } 166 167 func (c *tBtcWallet) PrivKeyForAddress(a btcutil.Address) (*btcec.PrivateKey, error) { 168 if c.privKeyForAddrErr != nil { 169 return nil, c.privKeyForAddrErr 170 } 171 return c.privKeyForAddr.PrivKey, nil 172 } 173 174 func (c *tBtcWallet) Database() walletdb.DB { 175 return nil 176 } 177 178 func (c *tBtcWallet) Unlock(passphrase []byte, lock <-chan time.Time) error { 179 return c.unlockErr 180 } 181 182 func (c *tBtcWallet) Lock() {} 183 184 func (c *tBtcWallet) Locked() bool { 185 return c.locked 186 } 187 188 func (c *tBtcWallet) SendOutputs(outputs []*wire.TxOut, keyScope *waddrmgr.KeyScope, 189 account uint32, minconf int32, satPerKb btcutil.Amount, 190 coinSelectionStrategy wallet.CoinSelectionStrategy, label string) (*wire.MsgTx, error) { 191 if c.sendToAddressErr != nil { 192 return nil, c.sendToAddressErr 193 } 194 tx := wire.NewMsgTx(wire.TxVersion) 195 for _, txOut := range outputs { 196 tx.AddTxOut(txOut) 197 } 198 tx.AddTxIn(dummyInput()) 199 200 return tx, nil 201 } 202 203 func (c *tBtcWallet) HaveAddress(a btcutil.Address) (bool, error) { 204 if c.ownedAddresses != nil && c.ownedAddresses[a.String()] { 205 return true, nil 206 } 207 return c.ownsAddress, nil 208 } 209 210 func (c *tBtcWallet) Stop() {} 211 212 func (c *tBtcWallet) Start() (SPVService, error) { 213 return nil, nil 214 } 215 216 func (c *tBtcWallet) WaitForShutdown() {} 217 218 func (c *tBtcWallet) ChainSynced() bool { 219 c.blockchainMtx.RLock() 220 defer c.blockchainMtx.RUnlock() 221 if c.getBlockchainInfo == nil { 222 return false 223 } 224 return c.getBlockchainInfo.Blocks >= c.getBlockchainInfo.Headers // -1 ok for chain sync ? 225 } 226 227 func (c *tBtcWallet) SynchronizeRPC(chainClient chain.Interface) {} 228 229 func (c *tBtcWallet) WalletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDetails, error) { 230 if c.getTransactionErr != nil { 231 return nil, c.getTransactionErr 232 } 233 var txData *GetTransactionResult 234 if c.getTransactionMap != nil { 235 if txData = c.getTransactionMap["any"]; txData == nil { 236 txData = c.getTransactionMap[txHash.String()] 237 } 238 } 239 if txData == nil { 240 return nil, WalletTransactionNotFound 241 } 242 243 tx, _ := msgTxFromBytes(txData.Bytes) 244 blockHash, _ := chainhash.NewHashFromStr(txData.BlockHash) 245 246 blk := c.getBlock(*blockHash) 247 var blockHeight int32 248 if blk != nil { 249 blockHeight = int32(blk.height) 250 } else { 251 blockHeight = -1 252 } 253 254 credits := make([]wtxmgr.CreditRecord, 0, len(tx.TxIn)) 255 debits := make([]wtxmgr.DebitRecord, 0, len(tx.TxIn)) 256 for i, in := range tx.TxIn { 257 credits = append(credits, wtxmgr.CreditRecord{ 258 // Amount:, 259 Index: uint32(i), 260 Spent: c.walletTxSpent, 261 // Change: , 262 }) 263 264 var debitAmount int64 265 // The sources of transaction inputs all need to be added to getTransactionMap 266 // in order to get accurate Fees and Amounts when calling GetWalletTransaction 267 // when using the SPV wallet. 268 if gtr := c.getTransactionMap[in.PreviousOutPoint.Hash.String()]; gtr != nil { 269 tx, _ := msgTxFromBytes(gtr.Bytes) 270 debitAmount = tx.TxOut[in.PreviousOutPoint.Index].Value 271 } 272 273 debits = append(debits, wtxmgr.DebitRecord{ 274 Amount: btcutil.Amount(debitAmount), 275 }) 276 277 } 278 return &wtxmgr.TxDetails{ 279 TxRecord: wtxmgr.TxRecord{ 280 MsgTx: *tx, 281 Received: time.Unix(int64(txData.Time), 0), 282 }, 283 Block: wtxmgr.BlockMeta{ 284 Block: wtxmgr.Block{ 285 Hash: *blockHash, 286 Height: blockHeight, 287 }, 288 }, 289 Credits: credits, 290 Debits: debits, 291 }, nil 292 } 293 294 func (c *tBtcWallet) SyncedTo() waddrmgr.BlockStamp { 295 bestHash, bestHeight := c.bestBlock() // NOTE: in reality this may be lower than the chain service's best block 296 blk := c.getBlock(*bestHash) 297 return waddrmgr.BlockStamp{ 298 Height: int32(bestHeight), 299 Hash: *bestHash, 300 Timestamp: blk.msgBlock.Header.Timestamp, 301 } 302 } 303 304 func (c *tBtcWallet) SignTx(tx *wire.MsgTx) error { 305 if c.signTxErr != nil { 306 return c.signTxErr 307 } 308 c.signFunc(tx) 309 if c.sigIncomplete { 310 return errors.New("tBtcWallet SignTransaction error") 311 } 312 return nil 313 } 314 315 func (c *tBtcWallet) AccountProperties(scope waddrmgr.KeyScope, acct uint32) (*waddrmgr.AccountProperties, error) { 316 return nil, nil 317 } 318 319 func (c *tBtcWallet) BlockNotifications(ctx context.Context) <-chan *BlockNotification { 320 return nil 321 } 322 323 func (c *tBtcWallet) ForceRescan() {} 324 325 func (c *tBtcWallet) RescanAsync() error { return nil } 326 327 func (c *tBtcWallet) Birthday() time.Time { 328 return time.Time{} 329 } 330 331 func (c *tBtcWallet) Reconfigure(*asset.WalletConfig, string) (bool, error) { 332 return false, nil 333 } 334 335 func (c *tBtcWallet) Peers() ([]*asset.WalletPeer, error) { 336 return nil, nil 337 } 338 func (c *tBtcWallet) AddPeer(string) error { 339 return nil 340 } 341 func (c *tBtcWallet) RemovePeer(string) error { 342 return nil 343 } 344 345 func (c *tBtcWallet) TotalReceivedForAddr(addr btcutil.Address, minConf int32) (btcutil.Amount, error) { 346 return 0, nil 347 } 348 349 type tNeutrinoClient struct { 350 *testData 351 } 352 353 func (c *tNeutrinoClient) Stop() error { return nil } 354 355 func (c *tNeutrinoClient) GetBlockHash(blockHeight int64) (*chainhash.Hash, error) { 356 c.blockchainMtx.RLock() 357 defer c.blockchainMtx.RUnlock() 358 for height, blockHash := range c.mainchain { 359 if height == blockHeight { 360 return blockHash, nil 361 } 362 } 363 return nil, fmt.Errorf("no (test) block at height %d", blockHeight) 364 } 365 366 func (c *tNeutrinoClient) BestBlock() (*headerfs.BlockStamp, error) { 367 c.blockchainMtx.RLock() 368 if c.getBestBlockHashErr != nil { 369 defer c.blockchainMtx.RUnlock() // reading c.getBestBlockHashErr below 370 return nil, c.getBestBlockHashErr 371 } 372 c.blockchainMtx.RUnlock() 373 bestHash, bestHeight := c.bestBlock() 374 blk := c.getBlock(*bestHash) 375 return &headerfs.BlockStamp{ 376 Height: int32(bestHeight), 377 Hash: *bestHash, 378 Timestamp: blk.msgBlock.Header.Timestamp, // neutrino sets this as of 88c025e 379 }, nil 380 } 381 382 func (c *tNeutrinoClient) Peers() []SPVPeer { 383 c.blockchainMtx.RLock() 384 defer c.blockchainMtx.RUnlock() 385 peer := &neutrino.ServerPeer{Peer: &peer.Peer{}} 386 if c.getBlockchainInfo != nil { 387 peer.UpdateLastBlockHeight(int32(c.getBlockchainInfo.Headers)) 388 } 389 return []SPVPeer{peer} 390 } 391 392 func (c *tNeutrinoClient) AddPeer(string) error { 393 return nil 394 } 395 396 func (c *tNeutrinoClient) GetBlockHeight(hash *chainhash.Hash) (int32, error) { 397 block := c.getBlock(*hash) 398 if block == nil { 399 return 0, fmt.Errorf("(test) block not found for block hash %s", hash) 400 } 401 return int32(block.height), nil 402 } 403 404 func (c *tNeutrinoClient) GetBlockHeader(blkHash *chainhash.Hash) (*wire.BlockHeader, error) { 405 block := c.getBlock(*blkHash) 406 if block == nil { 407 return nil, errors.New("no block verbose found") 408 } 409 return &block.msgBlock.Header, nil 410 } 411 412 func (c *tNeutrinoClient) GetCFilter(blockHash chainhash.Hash, filterType wire.FilterType, options ...neutrino.QueryOption) (*gcs.Filter, error) { 413 var key [gcs.KeySize]byte 414 copy(key[:], blockHash.CloneBytes()[:]) 415 scripts := c.getCFilterScripts[blockHash] 416 scripts = append(scripts, encode.RandomBytes(10)) 417 return gcs.BuildGCSFilter(builder.DefaultP, builder.DefaultM, key, scripts) 418 } 419 420 func (c *tNeutrinoClient) GetBlock(blockHash chainhash.Hash, options ...neutrino.QueryOption) (*btcutil.Block, error) { 421 blk := c.getBlock(blockHash) 422 if blk == nil { 423 return nil, fmt.Errorf("no (test) block %s", blockHash) 424 } 425 return btcutil.NewBlock(blk.msgBlock), nil 426 } 427 428 type tSPVPeer struct { 429 startHeight, lastHeight int32 430 } 431 432 func (p *tSPVPeer) StartingHeight() int32 { 433 return p.startHeight 434 } 435 436 func (p *tSPVPeer) LastBlock() int32 { 437 return p.lastHeight 438 } 439 440 func TestSwapConfirmations(t *testing.T) { 441 wallet, node, shutdown := tNewWallet(true, walletTypeSPV) 442 defer shutdown() 443 444 spv := wallet.node.(*spvWallet) 445 node.txOutRes = nil 446 447 const tipHeight = 10 448 const swapHeight = 1 449 const spendHeight = 3 450 const swapConfs = tipHeight - swapHeight + 1 451 452 for i := int64(0); i <= tipHeight; i++ { 453 node.addRawTx(i, dummyTx()) 454 } 455 456 _, _, pkScript, _, _, _, _ := makeSwapContract(true, time.Hour*12) 457 458 swapTx := makeRawTx([]dex.Bytes{pkScript}, []*wire.TxIn{dummyInput()}) 459 swapTxHash := swapTx.TxHash() 460 const vout = 0 461 swapOutPt := NewOutPoint(&swapTxHash, vout) 462 swapBlockHash, _ := node.addRawTx(swapHeight, swapTx) 463 464 spendTx := dummyTx() 465 spendTx.TxIn[0].PreviousOutPoint.Hash = swapTxHash 466 spendBlockHash, _ := node.addRawTx(spendHeight, spendTx) 467 spendTxHash := spendTx.TxHash() 468 469 matchTime := time.Unix(1, 0) 470 471 checkSuccess := func(tag string, expConfs uint32, expSpent bool) { 472 t.Helper() 473 confs, spent, err := spv.SwapConfirmations(&swapTxHash, vout, pkScript, matchTime) 474 if err != nil { 475 t.Fatalf("%s error: %v", tag, err) 476 } 477 if confs != expConfs { 478 t.Fatalf("wrong number of %s confs. wanted %d, got %d", tag, expConfs, confs) 479 } 480 if spent != expSpent { 481 t.Fatalf("%s path not expected spent status. wanted %t, got %t", tag, expSpent, spent) 482 } 483 } 484 485 checkFailure := func(tag string) { 486 t.Helper() 487 _, _, err := spv.SwapConfirmations(&swapTxHash, vout, pkScript, matchTime) 488 if err == nil { 489 t.Fatalf("no error for %q test", tag) 490 } 491 } 492 493 // confirmations() path 494 node.confsErr = tErr 495 checkFailure("confirmations") 496 497 node.confsErr = nil 498 node.confs = 10 499 node.confsSpent = true 500 txB, _ := serializeMsgTx(swapTx) 501 node.getTransactionMap = map[string]*GetTransactionResult{ 502 "any": { 503 BlockHash: swapBlockHash.String(), 504 BlockIndex: swapHeight, 505 Bytes: txB, 506 }} 507 node.walletTxSpent = true 508 checkSuccess("confirmations", swapConfs, true) 509 node.getTransactionMap = nil 510 node.walletTxSpent = false 511 node.confsErr = WalletTransactionNotFound 512 513 // DB path. 514 node.dbBlockForTx[swapTxHash] = &hashEntry{hash: *swapBlockHash} 515 node.dbBlockForTx[spendTxHash] = &hashEntry{hash: *spendBlockHash} 516 node.checkpoints[swapOutPt] = &ScanCheckpoint{ 517 Res: &FilterScanResult{ 518 BlockHash: swapBlockHash, 519 BlockHeight: swapHeight, 520 Spend: &SpendingInput{}, 521 Checkpoint: *spendBlockHash, 522 }, 523 } 524 checkSuccess("GetSpend", swapConfs, true) 525 delete(node.checkpoints, swapOutPt) 526 delete(node.dbBlockForTx, swapTxHash) 527 528 // Neutrino scan 529 530 // Fail cuz no match time provided and block not known. 531 matchTime = time.Time{} 532 checkFailure("no match time") 533 matchTime = time.Unix(1, 0) 534 535 // spent 536 node.getCFilterScripts[*spendBlockHash] = [][]byte{pkScript} 537 delete(node.checkpoints, swapOutPt) 538 checkSuccess("spend", 0, true) 539 node.getCFilterScripts[*spendBlockHash] = nil 540 541 // Not found 542 delete(node.checkpoints, swapOutPt) 543 checkFailure("no utxo") 544 545 // Found. 546 node.getCFilterScripts[*swapBlockHash] = [][]byte{pkScript} 547 delete(node.checkpoints, swapOutPt) 548 checkSuccess("scan find", swapConfs, false) 549 } 550 551 func TestFindBlockForTime(t *testing.T) { 552 wallet, node, shutdown := tNewWallet(true, walletTypeSPV) 553 defer shutdown() 554 spv := wallet.node.(*spvWallet) 555 556 const tipHeight = 40 557 558 for i := 0; i <= tipHeight; i++ { 559 node.addRawTx(int64(i), dummyTx()) 560 } 561 562 const searchBlock = 35 563 matchTime := generateTestBlockTime(searchBlock) 564 const offsetBlock = searchBlock - testBlocksPerBlockTimeOffset 565 const startBlock = offsetBlock - medianTimeBlocks 566 height, err := spv.FindBlockForTime(matchTime) 567 if err != nil { 568 t.Fatalf("FindBlockForTime error: %v", err) 569 } 570 if height != startBlock { 571 t.Fatalf("wrong height. wanted %d, got %d", startBlock, height) 572 } 573 574 // But if we shift the startBlock time to > offsetBlock time, the window 575 // will continue down 11 more. 576 _, blk := node.getBlockAtHeight(startBlock) 577 blk.msgBlock.Header.Timestamp = generateTestBlockTime(offsetBlock) 578 height, err = spv.FindBlockForTime(matchTime) 579 if err != nil { 580 t.Fatalf("FindBlockForTime error for shifted start block: %v", err) 581 } 582 if height != startBlock-medianTimeBlocks { 583 t.Fatalf("wrong height. wanted %d, got %d", startBlock-11, height) 584 } 585 586 // And doing an early enough block just returns genesis 587 height, err = spv.FindBlockForTime(generateTestBlockTime(10)) 588 if err != nil { 589 t.Fatalf("FindBlockForTime error for genesis test: %v", err) 590 } 591 if height != 0 { 592 t.Fatalf("not genesis: height = %d", height) 593 } 594 595 // A time way in the future still returns at least the last 11 blocks. 596 height, err = spv.FindBlockForTime(generateTestBlockTime(100)) 597 if err != nil { 598 t.Fatalf("FindBlockForTime error for future test: %v", err) 599 } 600 // +1 because tip block is included here, as opposed to the shifted start 601 // block, where the shifted block wasn't included. 602 if height != tipHeight-medianTimeBlocks+1 { 603 t.Fatalf("didn't get tip - 11. wanted %d, got %d", tipHeight-medianTimeBlocks, height) 604 } 605 } 606 607 func TestGetTxOut(t *testing.T) { 608 wallet, node, shutdown := tNewWallet(true, walletTypeSPV) 609 defer shutdown() 610 spv := wallet.node.(*spvWallet) 611 612 _, _, pkScript, _, _, _, _ := makeSwapContract(true, time.Hour*12) 613 const vout = 0 614 const blockHeight = 10 615 const tipHeight = 20 616 tx := makeRawTx([]dex.Bytes{pkScript}, []*wire.TxIn{dummyInput()}) 617 txHash := tx.TxHash() 618 outPt := NewOutPoint(&txHash, vout) 619 blockHash, _ := node.addRawTx(blockHeight, tx) 620 txB, _ := serializeMsgTx(tx) 621 node.addRawTx(tipHeight, dummyTx()) 622 spendingTx := dummyTx() 623 spendingTx.TxIn[0].PreviousOutPoint.Hash = txHash 624 spendBlockHash, _ := node.addRawTx(tipHeight-1, spendingTx) 625 626 // Prime the db 627 for h := int64(1); h <= tipHeight; h++ { 628 node.addRawTx(h, dummyTx()) 629 } 630 631 // Abnormal error 632 node.getTransactionErr = tErr 633 _, _, err := spv.GetTxOut(&txHash, vout, pkScript, generateTestBlockTime(blockHeight)) 634 if err == nil { 635 t.Fatalf("no error for getWalletTransaction error") 636 } 637 638 // Wallet transaction found 639 node.getTransactionErr = nil 640 node.getTransactionMap = map[string]*GetTransactionResult{"any": { 641 BlockHash: blockHash.String(), 642 Bytes: txB, 643 }} 644 645 _, confs, err := spv.GetTxOut(&txHash, vout, pkScript, generateTestBlockTime(blockHeight)) 646 if err != nil { 647 t.Fatalf("error for wallet transaction found: %v", err) 648 } 649 if confs != tipHeight-blockHeight+1 { 650 t.Fatalf("wrong confs for wallet transaction. wanted %d, got %d", tipHeight-blockHeight+1, confs) 651 } 652 653 // No wallet transaction, but we have a spend recorded. 654 node.getTransactionErr = WalletTransactionNotFound 655 node.getTransactionMap = nil 656 node.checkpoints[outPt] = &ScanCheckpoint{ 657 Res: &FilterScanResult{ 658 BlockHash: blockHash, 659 Spend: &SpendingInput{}, 660 Checkpoint: *spendBlockHash, 661 }} 662 op, confs, err := spv.GetTxOut(&txHash, vout, pkScript, generateTestBlockTime(blockHeight)) 663 if op != nil || confs != 0 || err != nil { 664 t.Fatal("wrong result for spent txout", op != nil, confs, err) 665 } 666 delete(node.checkpoints, outPt) 667 668 // no spend record. gotta scan 669 670 // case 1: we have a block hash in the database 671 node.dbBlockForTx[txHash] = &hashEntry{hash: *blockHash} 672 node.getCFilterScripts[*blockHash] = [][]byte{pkScript} 673 _, _, err = spv.GetTxOut(&txHash, vout, pkScript, generateTestBlockTime(blockHeight)) 674 if err != nil { 675 t.Fatalf("error for GetUtxo with cached hash: %v", err) 676 } 677 678 // case 2: no block hash in db. Will do scan and store them. 679 delete(node.dbBlockForTx, txHash) 680 delete(node.checkpoints, outPt) 681 _, _, err = spv.GetTxOut(&txHash, vout, pkScript, generateTestBlockTime(blockHeight)) 682 if err != nil { 683 t.Fatalf("error for GetUtxo with no cached hash: %v", err) 684 } 685 if _, inserted := node.dbBlockForTx[txHash]; !inserted { 686 t.Fatalf("db not updated after GetUtxo scan success") 687 } 688 689 // case 3: spending tx found first 690 delete(node.checkpoints, outPt) 691 node.getCFilterScripts[*spendBlockHash] = [][]byte{pkScript} 692 txOut, _, err := spv.GetTxOut(&txHash, vout, pkScript, generateTestBlockTime(blockHeight)) 693 if err != nil { 694 t.Fatalf("error for spent tx: %v", err) 695 } 696 if txOut != nil { 697 t.Fatalf("spend output returned from getTxOut") 698 } 699 700 // Make sure we can find it with the checkpoint. 701 node.checkpoints[outPt].Res.Spend = nil 702 node.getCFilterScripts[*spendBlockHash] = nil 703 // We won't actually scan for the output itself, so nil'ing these should 704 // have no effect. 705 node.getCFilterScripts[*blockHash] = nil 706 _, _, err = spv.GetTxOut(&txHash, vout, pkScript, generateTestBlockTime(blockHeight)) 707 if err != nil { 708 t.Fatalf("error for checkpointed output: %v", err) 709 } 710 } 711 712 func TestTryBlocksWithNotifier(t *testing.T) { 713 defaultWalletBlockAllowance := walletBlockAllowance 714 defaultBlockTicker := blockTicker 715 716 walletBlockAllowance = 50 * time.Millisecond 717 blockTicker = 20 * time.Millisecond 718 719 defer func() { 720 walletBlockAllowance = defaultWalletBlockAllowance 721 blockTicker = defaultBlockTicker 722 }() 723 724 wallet, node, shutdown := tNewWallet(true, walletTypeSPV) 725 defer shutdown() 726 727 spv := wallet.node.(*spvWallet) 728 729 getNote := func(timeout time.Duration) bool { 730 select { 731 case <-node.tipChanged: 732 return true 733 case <-time.After(timeout): 734 return false 735 } 736 } 737 738 if getNote(walletBlockAllowance * 2) { 739 t.Fatalf("got a first block") 740 } 741 742 // Feed (*baseWallet).watchBlocks: 743 // (*spvWallet).getBestBlockHeader -> (*spvWallet).getBestBlockHash -> (*tBtcWallet).syncedTo -> (*testData).bestBlock [mainchain] 744 // (*spvWallet).getBestBlockHeader -> (*spvWallet).getBlockHeader -> (*testData).getBlock [verboseBlocks] 745 746 var tipHeight int64 747 addBlock := func() *BlockVector { // update the mainchain and verboseBlocks testData fields 748 tipHeight++ 749 h, _ := node.addRawTx(tipHeight, dummyTx()) 750 return &BlockVector{tipHeight, *h} 751 } 752 753 // Start with no blocks so that we're not synced. 754 node.blockchainMtx.Lock() 755 node.getBlockchainInfo = &GetBlockchainInfoResult{ 756 Headers: 3, 757 Blocks: 0, 758 } 759 node.blockchainMtx.Unlock() 760 761 addBlock() 762 763 if !getNote(blockTicker * 2) { 764 t.Fatalf("0th block didn't send tip change update") 765 } 766 767 addBlock() 768 769 // It should not come through on the block tick, since it will be queued. 770 if getNote(blockTicker * 2) { 771 t.Fatalf("got non-0th block that should've been queued") 772 } 773 774 // And it won't come through after a single block allowance, because we're 775 // not synced. 776 if getNote(walletBlockAllowance * 2) { 777 t.Fatal("block didn't wait for the syncing mode allowance") 778 } 779 780 // But it will come through after the sync timeout = 10 * normal timeout. 781 if !getNote(walletBlockAllowance * 9) { 782 t.Fatal("block didn't time out in syncing mode") 783 } 784 785 // But if we're synced, it should come through after the normal block 786 // allowance. 787 addBlock() 788 node.blockchainMtx.Lock() 789 node.getBlockchainInfo = &GetBlockchainInfoResult{ 790 Headers: tipHeight, 791 Blocks: tipHeight, 792 } 793 node.blockchainMtx.Unlock() 794 if !getNote(walletBlockAllowance * 2) { 795 t.Fatal("block didn't time out in normal mode") 796 } 797 798 // On the other hand, a wallet block should come through immediately. Not 799 // even waiting on the block tick. 800 spv.tipChan <- addBlock() 801 if !getNote(blockTicker / 2) { 802 t.Fatal("wallet block wasn't sent through") 803 } 804 805 // If we do the same thing but make sure that a polled block is queued 806 // first, we should still see the block right away, and the queued block 807 // should be canceled. 808 blk := addBlock() 809 time.Sleep(blockTicker * 2) 810 spv.tipChan <- blk 811 if !getNote(blockTicker / 2) { 812 t.Fatal("wallet block wasn't sent through with polled block queued") 813 } 814 815 if getNote(walletBlockAllowance * 2) { 816 t.Fatal("queued polled block that should have been canceled came through") 817 } 818 } 819 820 func TestGetBlockHeader(t *testing.T) { 821 wallet, node, shutdown := tNewWallet(true, walletTypeSPV) 822 defer shutdown() 823 824 const tipHeight = 12 825 const blockHeight = 11 // 2 confirmations 826 var blockHash, prevHash, h chainhash.Hash 827 var blockHdr msgBlockWithHeight 828 for height := 0; height <= tipHeight; height++ { 829 hdr := &msgBlockWithHeight{ 830 msgBlock: &wire.MsgBlock{ 831 Header: wire.BlockHeader{ 832 PrevBlock: h, 833 Timestamp: time.Unix(int64(height), 0), 834 }, 835 Transactions: nil, 836 }, 837 height: int64(height), 838 } 839 840 h = hdr.msgBlock.BlockHash() 841 842 switch height { 843 case blockHeight - 1: 844 prevHash = h 845 blockHdr = *hdr 846 case blockHeight: 847 blockHash = h 848 } 849 850 node.verboseBlocks[h] = hdr 851 hh := h // just because we are storing pointers in mainchain 852 node.mainchain[int64(height)] = &hh 853 } 854 855 hdr, mainchain, err := wallet.tipRedeemer.GetBlockHeader(&blockHash) 856 if err != nil { 857 t.Fatalf("initial success error: %v", err) 858 } 859 if !mainchain { 860 t.Fatalf("expected block %s to be in mainchain", blockHash) 861 } 862 if hdr.Hash != blockHash.String() { 863 t.Fatal("wrong header?") 864 } 865 if hdr.Height != blockHeight { 866 t.Fatal("wrong height?") 867 } 868 if hdr.Time != blockHeight { 869 t.Fatalf("wrong block time stamp, wanted %d, got %d", blockHeight, hdr.Time) 870 } 871 if hdr.Confirmations != 2 { 872 t.Fatalf("expected 2 confs, got %d", hdr.Confirmations) 873 } 874 if hdr.PreviousBlockHash != prevHash.String() { 875 t.Fatalf("wrong previous hash, want: %s got: %s", prevHash.String(), hdr.PreviousBlockHash) 876 } 877 878 node.mainchain[int64(blockHeight)] = &chainhash.Hash{0x01} // mainchain ended up with different block 879 hdr, mainchain, err = wallet.tipRedeemer.GetBlockHeader(&blockHash) 880 if err != nil { 881 t.Fatalf("initial success error: %v", err) 882 } 883 if mainchain { 884 t.Fatalf("expected block %s to be classified as oprhan", blockHash) 885 } 886 if hdr.Confirmations != -1 { 887 t.Fatalf("expected -1 confs for side chain block, got %d", hdr.Confirmations) 888 } 889 node.mainchain[int64(blockHeight)] = &blockHash // clean up 890 891 // Can't fetch header error. 892 delete(node.verboseBlocks, blockHash) // can't find block by hash 893 if _, _, err := wallet.tipRedeemer.GetBlockHeader(&blockHash); err == nil { 894 t.Fatalf("Can't fetch header error not propagated") 895 } 896 node.verboseBlocks[blockHash] = &blockHdr // clean up 897 898 // Main chain is shorter than requested block. 899 prevMainchain := node.mainchain 900 node.mainchain = map[int64]*chainhash.Hash{ 901 0: node.mainchain[0], 902 1: node.mainchain[1], 903 } 904 hdr, mainchain, err = wallet.tipRedeemer.GetBlockHeader(&blockHash) 905 if err != nil { 906 t.Fatalf("invalid tip height not noticed") 907 } 908 if mainchain { 909 t.Fatalf("expected block %s to be classified as orphan", blockHash) 910 } 911 if hdr.Confirmations != -1 { 912 t.Fatalf("confirmations not zero for lower mainchain tip height, got: %d", hdr.Confirmations) 913 } 914 node.mainchain = prevMainchain // clean up 915 }