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