diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 8381f0048a..552adf283f 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -77,6 +77,7 @@ const ( var ( // blockTicker is the delay between calls to check for new blocks. blockTicker = time.Second + walletBlockAllowance = time.Second * 10 conventionalConversionFactor = float64(dexbtc.UnitInfo.Conventional.ConversionFactor) rpcOpts = []*asset.ConfigOption{ { @@ -521,7 +522,7 @@ type ExchangeWallet struct { type block struct { height int64 - hash string + hash chainhash.Hash } // findRedemptionReq represents a request to find a contract's redemption, @@ -754,7 +755,7 @@ func (btc *ExchangeWallet) Connect(ctx context.Context) (*sync.WaitGroup, error) return nil, err } // Initialize the best block. - h, err := btc.node.getBestBlockHash() + bestBlockHash, err := btc.node.getBestBlockHash() if err != nil { return nil, fmt.Errorf("error initializing best block for %s: %w", btc.symbol, err) } @@ -765,7 +766,7 @@ func (btc *ExchangeWallet) Connect(ctx context.Context) (*sync.WaitGroup, error) } btc.tipMtx.Lock() - btc.currentTip, err = btc.blockFromHash(h.String()) + btc.currentTip, err = btc.blockFromHash(bestBlockHash) btc.tipMtx.Unlock() if err != nil { return nil, fmt.Errorf("error parsing best block for %s: %w", btc.symbol, err) @@ -774,7 +775,7 @@ func (btc *ExchangeWallet) Connect(ctx context.Context) (*sync.WaitGroup, error) wg.Add(1) go func() { defer wg.Done() - btc.run(ctx) + btc.watchBlocks(ctx) btc.shutdown() }() return &wg, nil @@ -1930,7 +1931,7 @@ func (btc *ExchangeWallet) LocktimeExpired(contract dex.Bytes) (bool, time.Time, if err != nil { return false, time.Time{}, fmt.Errorf("get best block hash error: %w", err) } - bestBlockHeader, err := btc.node.getBlockHeader(bestBlockHash.String()) + bestBlockHeader, err := btc.node.getBlockHeader(bestBlockHash) if err != nil { return false, time.Time{}, fmt.Errorf("get best block header error: %w", err) } @@ -2390,15 +2391,98 @@ func (btc *ExchangeWallet) RegFeeConfirmations(_ context.Context, id dex.Bytes) return uint32(tx.Confirmations), nil } -// run pings for new blocks and runs the tipChange callback function when the -// block changes. -func (btc *ExchangeWallet) run(ctx context.Context) { +// watchBlocks pings for new blocks and runs the tipChange callback function +// when the block changes. +func (btc *ExchangeWallet) watchBlocks(ctx context.Context) { ticker := time.NewTicker(blockTicker) defer ticker.Stop() + + var walletBlock <-chan *block + if notifier, isNotifier := btc.node.(tipNotifier); isNotifier { + walletBlock = notifier.tipFeed() + } + + // A polledBlock is a block found during polling, but whose broadcast has + // been queued in anticipation of a wallet notification. + type polledBlock struct { + *block + queue *time.Timer + } + + // queuedBlock is the currently queued, polling-discovered block that will + // be broadcast after a timeout if the wallet doesn't send the matching + // notification. + var queuedBlock *polledBlock + for { select { + + // Poll for the block. If the wallet offers tip reports, delay reporting + // the tip to give the wallet a moment to request and scan block data. case <-ticker.C: - btc.checkForNewBlocks(ctx) + newTipHash, err := btc.node.getBestBlockHash() + if err != nil { + go btc.tipChange(fmt.Errorf("failed to get best block hash from %s node", btc.symbol)) + return + } + + if queuedBlock != nil && *newTipHash == queuedBlock.block.hash { + continue + } + + btc.tipMtx.RLock() + sameTip := btc.currentTip.hash == *newTipHash + btc.tipMtx.RUnlock() + if sameTip { + continue + } + + newTip, err := btc.blockFromHash(newTipHash) + if err != nil { + go btc.tipChange(fmt.Errorf("error setting new tip: %w", err)) + } + + // If the wallet is not offering tip reports, send this one right + // away. + if walletBlock == nil { + btc.reportNewTip(ctx, newTip) + } else { + // Queue it for reporting, but don't send it right away. Give the + // wallet a chance to provide their block update. SPV wallet may + // need more time after storing the block header to fetch and + // scan filters and issue the FilteredBlockConnected report. + if queuedBlock != nil { + queuedBlock.queue.Stop() + } + blockAllowance := walletBlockAllowance + syncStatus, err := btc.node.syncStatus() + if err != nil { + btc.log.Errorf("Error retreiving sync status before queuing polled block: %v", err) + } else if syncStatus.Syncing { + blockAllowance *= 10 + } + queuedBlock = &polledBlock{ + block: newTip, + queue: time.AfterFunc(blockAllowance, func() { + btc.log.Warnf("Reporting a block found in polling that the wallet apparently "+ + "never reported: %d %s. If you see this message repeatedly, it may indicate "+ + "an issue with the wallet.", newTip.height, newTip.hash) + btc.reportNewTip(ctx, newTip) + }), + } + } + + // Tip reports from the wallet are always sent, and we'll clear any + // queued polled block that would appear to be superceded by this one. + case walletTip := <-walletBlock: + if queuedBlock != nil && walletTip.height >= queuedBlock.height { + if !queuedBlock.queue.Stop() && walletTip.hash == queuedBlock.hash { + continue + } + queuedBlock = nil + } + btc.reportNewTip(ctx, walletTip) + case <-ctx.Done(): return } @@ -2424,10 +2508,18 @@ func (btc *ExchangeWallet) prepareRedemptionRequestsForBlockCheck() []*findRedem return reqs } -// checkForNewBlocks checks for new blocks. When a tip change is detected, the -// tipChange callback function is invoked and a goroutine is started to check -// if any contracts in the findRedemptionQueue are redeemed in the new blocks. -func (btc *ExchangeWallet) checkForNewBlocks(ctx context.Context) { +// reportNewTip sets the currentTip. The tipChange callback function is invoked +// and a goroutine is started to check if any contracts in the +// findRedemptionQueue are redeemed in the new blocks. +func (btc *ExchangeWallet) reportNewTip(ctx context.Context, newTip *block) { + btc.tipMtx.Lock() + defer btc.tipMtx.Unlock() + + prevTip := btc.currentTip + btc.currentTip = newTip + btc.log.Debugf("tip change: %d (%s) => %d (%s)", prevTip.height, prevTip.hash, newTip.height, newTip.hash) + go btc.tipChange(nil) + reqs := btc.prepareRedemptionRequestsForBlockCheck() // Redemption search would be compromised if the starting point cannot // be determined, as searching just the new tip might result in blocks @@ -2439,41 +2531,12 @@ func (btc *ExchangeWallet) checkForNewBlocks(ctx context.Context) { } } - newTipHash, err := btc.node.getBestBlockHash() - if err != nil { - go btc.tipChange(fmt.Errorf("failed to get best block hash from %s node", btc.symbol)) - return - } - - // This method is called frequently. Don't hold write lock - // unless tip has changed. - btc.tipMtx.RLock() - sameTip := btc.currentTip.hash == newTipHash.String() - btc.tipMtx.RUnlock() - if sameTip { - return - } - - btc.tipMtx.Lock() - defer btc.tipMtx.Unlock() - - newTip, err := btc.blockFromHash(newTipHash.String()) - if err != nil { - go btc.tipChange(fmt.Errorf("error setting new tip: %w", err)) - return - } - - prevTip := btc.currentTip - btc.currentTip = newTip - btc.log.Debugf("tip change: %d (%s) => %d (%s)", prevTip.height, prevTip.hash, newTip.height, newTip.hash) - go btc.tipChange(nil) - var startPoint *block // Check if the previous tip is still part of the mainchain (prevTip confs >= 0). // Redemption search would typically resume from prevTipHeight + 1 unless the // previous tip was re-orged out of the mainchain, in which case redemption // search will resume from the mainchain ancestor of the previous tip. - prevTipHeader, err := btc.node.getBlockHeader(prevTip.hash) + prevTipHeader, err := btc.node.getBlockHeader(&prevTip.hash) switch { case err != nil: // Redemption search cannot continue reliably without knowing if there @@ -2486,7 +2549,11 @@ func (btc *ExchangeWallet) checkForNewBlocks(ctx context.Context) { // The previous tip is no longer part of the mainchain. Crawl blocks // backwards until finding a mainchain block. Start with the block // that is the immediate ancestor to the previous tip. - ancestorBlockHash := prevTipHeader.PreviousBlockHash + ancestorBlockHash, err := chainhash.NewHashFromStr(prevTipHeader.PreviousBlockHash) + if err != nil { + notifyFatalFindRedemptionError("hash decode error for block %s: %w", prevTipHeader.PreviousBlockHash, err) + return + } for { aBlock, err := btc.node.getBlockHeader(ancestorBlockHash) if err != nil { @@ -2495,7 +2562,7 @@ func (btc *ExchangeWallet) checkForNewBlocks(ctx context.Context) { } if aBlock.Confirmations > -1 { // Found the mainchain ancestor of previous tip. - startPoint = &block{height: aBlock.Height, hash: aBlock.Hash} + startPoint = &block{height: aBlock.Height, hash: *ancestorBlockHash} btc.log.Debugf("reorg detected from height %d to %d", aBlock.Height, newTip.height) break } @@ -2505,7 +2572,11 @@ func (btc *ExchangeWallet) checkForNewBlocks(ctx context.Context) { notifyFatalFindRedemptionError("no mainchain ancestor for orphaned block %s", prevTipHeader.Hash) return } - ancestorBlockHash = aBlock.PreviousBlockHash + ancestorBlockHash, err = chainhash.NewHashFromStr(aBlock.PreviousBlockHash) + if err != nil { + notifyFatalFindRedemptionError("hash decode error for block %s: %w", prevTipHeader.PreviousBlockHash, err) + return + } } case newTip.height-prevTipHeader.Height > 1: @@ -2516,7 +2587,7 @@ func (btc *ExchangeWallet) checkForNewBlocks(ctx context.Context) { notifyFatalFindRedemptionError("getBlockHash error for height %d: %w", afterPrivTip, err) return } - startPoint = &block{hash: hashAfterPrevTip.String(), height: afterPrivTip} + startPoint = &block{hash: *hashAfterPrevTip, height: afterPrivTip} default: // Just 1 new block since last tip report, search the lone block. @@ -2524,8 +2595,7 @@ func (btc *ExchangeWallet) checkForNewBlocks(ctx context.Context) { } if len(reqs) > 0 { - startHash, _ := chainhash.NewHashFromStr(startPoint.hash) - go btc.tryRedemptionRequests(ctx, startHash, reqs) + go btc.tryRedemptionRequests(ctx, &startPoint.hash, reqs) } } @@ -2598,12 +2668,12 @@ out: return blockHeight, nil } -func (btc *ExchangeWallet) blockFromHash(hash string) (*block, error) { +func (btc *ExchangeWallet) blockFromHash(hash *chainhash.Hash) (*block, error) { blk, err := btc.node.getBlockHeader(hash) if err != nil { return nil, fmt.Errorf("getBlockHeader error for hash %s: %w", hash, err) } - return &block{hash: hash, height: blk.Height}, nil + return &block{hash: *hash, height: blk.Height}, nil } // convertCoin converts the asset.Coin to an output. diff --git a/client/asset/btc/btc_test.go b/client/asset/btc/btc_test.go index 9265870d50..91faaf710e 100644 --- a/client/asset/btc/btc_test.go +++ b/client/asset/btc/btc_test.go @@ -133,10 +133,11 @@ type testData struct { signFunc func(*wire.MsgTx) signMsgFunc func([]json.RawMessage) (json.RawMessage, error) - blockchainMtx sync.RWMutex - verboseBlocks map[string]*msgBlockWithHeight - dbBlockForTx map[chainhash.Hash]*hashEntry - mainchain map[int64]*chainhash.Hash + blockchainMtx sync.RWMutex + verboseBlocks map[string]*msgBlockWithHeight + dbBlockForTx map[chainhash.Hash]*hashEntry + mainchain map[int64]*chainhash.Hash + getBlockchainInfo *getBlockchainInfoResult getBestBlockHashErr error mempoolTxs map[chainhash.Hash]*wire.MsgTx @@ -157,7 +158,6 @@ type testData struct { getTransaction *GetTransactionResult getTransactionErr error - getBlockchainInfo *getBlockchainInfoResult getBlockchainInfoErr error unlockErr error lockErr error @@ -167,6 +167,7 @@ type testData struct { signTxErr error listUnspent []*ListUnspentResult listUnspentErr error + tipChanged chan struct{} // spv fetchInputInfoTx *wire.MsgTx @@ -195,6 +196,7 @@ func newTestData() *testData { getCFilterScripts: make(map[chainhash.Hash][][]byte), confsErr: WalletTransactionNotFound, checkpoints: make(map[outPoint]*scanCheckpoint), + tipChanged: make(chan struct{}, 1), } } @@ -410,6 +412,8 @@ func (c *tRawRequester) RawRequest(_ context.Context, method string, params []js case methodGetTransaction: return encodeOrError(c.getTransaction, c.getTransactionErr) case methodGetBlockchainInfo: + c.blockchainMtx.RLock() + defer c.blockchainMtx.RUnlock() return encodeOrError(c.getBlockchainInfo, c.getBlockchainInfoErr) case methodLock: return nil, c.lockErr @@ -567,7 +571,12 @@ func tNewWallet(segwit bool, walletType string) (*ExchangeWallet, *testData, fun data := newTestData() walletCfg := &asset.WalletConfig{ - TipChange: func(error) {}, + TipChange: func(error) { + select { + case data.tipChanged <- struct{}{}: + default: + } + }, } walletCtx, shutdown := context.WithCancel(tCtx) cfg := &BTCCloneCFG{ @@ -596,6 +605,7 @@ func tNewWallet(segwit bool, walletType string) (*ExchangeWallet, *testData, fun chainParams: &chaincfg.MainNetParams, wallet: &tBtcWallet{data}, cl: neutrinoClient, + tipChan: make(chan *block, 1), chainClient: nil, acctNum: 0, txBlocks: data.dbBlockForTx, @@ -617,12 +627,22 @@ func tNewWallet(segwit bool, walletType string) (*ExchangeWallet, *testData, fun return nil, nil, nil, err } wallet.tipMtx.Lock() - wallet.currentTip = &block{height: data.GetBestBlockHeight(), - hash: bestHash.String()} + wallet.currentTip = &block{ + height: data.GetBestBlockHeight(), + hash: *bestHash, + } wallet.tipMtx.Unlock() - go wallet.run(walletCtx) - - return wallet, data, shutdown, nil + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + wallet.watchBlocks(walletCtx) + }() + shutdownAndWait := func() { + shutdown() + wg.Wait() + } + return wallet, data, shutdownAndWait, nil } func mustMarshal(thing interface{}) []byte { @@ -2058,7 +2078,10 @@ func testFindRedemption(t *testing.T, segwit bool, walletType string) { node.getCFilterScripts[*redeemBlockHash] = [][]byte{pkScript} // Update currentTip from "RPC". Normally run() would do this. - wallet.checkForNewBlocks(tCtx) + wallet.reportNewTip(tCtx, &block{ + hash: *redeemBlockHash, + height: contractHeight + 2, + }) // Check find redemption result. _, checkSecret, err := wallet.FindRedemption(tCtx, coinID) @@ -2961,5 +2984,4 @@ func testTryRedemptionRequests(t *testing.T, segwit bool, walletType string) { } } } - } diff --git a/client/asset/btc/rpcclient.go b/client/asset/btc/rpcclient.go index 6cced815ae..f378dbf36b 100644 --- a/client/asset/btc/rpcclient.go +++ b/client/asset/btc/rpcclient.go @@ -221,7 +221,7 @@ func (wc *rpcClient) getBestBlockHeight() (int32, error) { if err != nil { return -1, err } - header, err := wc.getBlockHeader(tipHash.String()) + header, err := wc.getBlockHeader(tipHash) if err != nil { return -1, err } @@ -503,10 +503,10 @@ func (wc *rpcClient) swapConfirmations(txHash *chainhash.Hash, vout uint32, _ [] } // getBlockHeader gets the block header for the specified block hash. -func (wc *rpcClient) getBlockHeader(blockHash string) (*blockHeader, error) { +func (wc *rpcClient) getBlockHeader(blockHash *chainhash.Hash) (*blockHeader, error) { blkHeader := new(blockHeader) err := wc.call(methodGetBlockHeader, - anylist{blockHash, true}, blkHeader) + anylist{blockHash.String(), true}, blkHeader) if err != nil { return nil, err } @@ -514,8 +514,8 @@ func (wc *rpcClient) getBlockHeader(blockHash string) (*blockHeader, error) { } // getBlockHeight gets the mainchain height for the specified block. -func (wc *rpcClient) getBlockHeight(h *chainhash.Hash) (int32, error) { - hdr, err := wc.getBlockHeader(h.String()) +func (wc *rpcClient) getBlockHeight(blockHash *chainhash.Hash) (int32, error) { + hdr, err := wc.getBlockHeader(blockHash) if err != nil { return -1, err } diff --git a/client/asset/btc/spv.go b/client/asset/btc/spv.go index d2ee5a1d9e..f9d363f5e2 100644 --- a/client/asset/btc/spv.go +++ b/client/asset/btc/spv.go @@ -98,6 +98,7 @@ type btcWallet interface { walletTransaction(txHash *chainhash.Hash) (*wtxmgr.TxDetails, error) syncedTo() waddrmgr.BlockStamp signTransaction(*wire.MsgTx) error + txNotifications() wallet.TransactionNotificationsClient } var _ btcWallet = (*walletExtender)(nil) @@ -284,9 +285,13 @@ type spvWallet struct { log dex.Logger loader *wallet.Loader + + tipChan chan *block + syncTarget int32 } var _ Wallet = (*spvWallet)(nil) +var _ tipNotifier = (*spvWallet)(nil) // loadSPVWallet loads an existing wallet. func loadSPVWallet(dbDir string, logger dex.Logger, connectPeers []string, chainParams *chaincfg.Params) *spvWallet { @@ -299,9 +304,16 @@ func loadSPVWallet(dbDir string, logger dex.Logger, connectPeers []string, chain checkpoints: make(map[outPoint]*scanCheckpoint), log: logger, connectPeers: connectPeers, + tipChan: make(chan *block, 8), } } +// tipFeed satisfies the tipNotifier interface, signaling that *spvWallet +// will take precedence in sending block notifications. +func (w *spvWallet) tipFeed() <-chan *block { + return w.tipChan +} + // storeTxBlock stores the block hash for the tx in the cache. func (w *spvWallet) storeTxBlock(txHash, blockHash chainhash.Hash) { w.txBlocksMtx.Lock() @@ -468,6 +480,13 @@ func (w *spvWallet) syncStatus() (*syncStatus, error) { synced = true } + if atomic.SwapInt32(&w.syncTarget, target) == 0 && target > 0 { + w.tipChan <- &block{ + hash: blk.Hash, + height: int64(blk.Height), + } + } + return &syncStatus{ Target: target, Height: currentHeight, @@ -835,11 +854,7 @@ func (w *spvWallet) walletUnlock(pw []byte) error { return w.Unlock(pw) } -func (w *spvWallet) getBlockHeader(hashStr string) (*blockHeader, error) { - blockHash, err := chainhash.NewHashFromStr(hashStr) - if err != nil { - return nil, err - } +func (w *spvWallet) getBlockHeader(blockHash *chainhash.Hash) (*blockHeader, error) { hdr, err := w.cl.GetBlockHeader(blockHash) if err != nil { return nil, err @@ -913,20 +928,14 @@ func (w *spvWallet) connect(ctx context.Context, wg *sync.WaitGroup) error { return err } - // Possible to subscribe to block notifications here with a NewRescan -> - // *Rescan supplied with a QuitChan-type RescanOption. - // Actually, should use btcwallet.Wallet.NtfnServer ? - - // notes := make(<-chan interface{}) - // if w.chainClient != nil { - // notes = w.chainClient.Notifications() - // } + txNotes := w.wallet.txNotifications() // Nanny for the caches checkpoints and txBlocks caches. wg.Add(1) go func() { defer wg.Done() defer w.stop() + defer txNotes.Done() ticker := time.NewTicker(time.Minute * 20) defer ticker.Stop() @@ -949,8 +958,32 @@ func (w *spvWallet) connect(ctx context.Context, wg *sync.WaitGroup) error { } } w.checkpointMtx.Unlock() - // case note := <-notes: - // fmt.Printf("--Notification received: %T: %+v \n", note, note) + + case note := <-txNotes.C: + if len(note.AttachedBlocks) > 0 { + lastBlock := note.AttachedBlocks[len(note.AttachedBlocks)-1] + syncTarget := atomic.LoadInt32(&w.syncTarget) + + for ib := range note.AttachedBlocks { + for _, nt := range note.AttachedBlocks[ib].Transactions { + w.log.Debugf("Block %d contains wallet transaction %v", note.AttachedBlocks[ib].Height, nt.Hash) + } + } + + if syncTarget == 0 || (lastBlock.Height < syncTarget && lastBlock.Height%10_000 != 0) { + continue + } + + select { + case w.tipChan <- &block{ + hash: *lastBlock.Hash, + height: int64(lastBlock.Height), + }: + default: + w.log.Warnf("tip report channel was blocking") + } + } + case <-ctx.Done(): return } @@ -1650,6 +1683,11 @@ func (w *walletExtender) signTransaction(tx *wire.MsgTx) error { }) } +// txNotifications gives access to the NotificationServer's tx notifications. +func (w *walletExtender) txNotifications() wallet.TransactionNotificationsClient { + return w.NtfnServer.TransactionNotifications() +} + // secretSource is used to locate keys and redemption scripts while signing a // transaction. secretSource satisfies the txauthor.SecretsSource interface. type secretSource struct { diff --git a/client/asset/btc/spv_test.go b/client/asset/btc/spv_test.go index fbee237db2..dd69e4caab 100644 --- a/client/asset/btc/spv_test.go +++ b/client/asset/btc/spv_test.go @@ -9,6 +9,7 @@ package btc import ( "errors" "fmt" + "sync/atomic" "testing" "time" @@ -190,6 +191,8 @@ func (c *tBtcWallet) Stop() {} func (c *tBtcWallet) WaitForShutdown() {} func (c *tBtcWallet) ChainSynced() bool { + c.blockchainMtx.RLock() + defer c.blockchainMtx.RUnlock() if c.getBlockchainInfo == nil { return false } @@ -265,6 +268,10 @@ func (c *tBtcWallet) signTransaction(tx *wire.MsgTx) error { return nil } +func (c *tBtcWallet) txNotifications() wallet.TransactionNotificationsClient { + return wallet.TransactionNotificationsClient{} +} + type tNeutrinoClient struct { *testData } @@ -294,6 +301,8 @@ func (c *tNeutrinoClient) BestBlock() (*headerfs.BlockStamp, error) { } func (c *tNeutrinoClient) Peers() []*neutrino.ServerPeer { + c.blockchainMtx.RLock() + defer c.blockchainMtx.RUnlock() peer := &neutrino.ServerPeer{Peer: &peer.Peer{}} if c.getBlockchainInfo != nil { peer.UpdateLastBlockHeight(int32(c.getBlockchainInfo.Headers)) @@ -699,3 +708,102 @@ func TestSendWithSubtract(t *testing.T) { t.Fatalf("test passed with fees > available error") } } + +func TestTryBlocksWithNotifier(t *testing.T) { + defaultWalletBlockAllowance := walletBlockAllowance + defaultBlockTicker := blockTicker + + walletBlockAllowance = 30 * time.Millisecond + blockTicker = 5 * time.Millisecond + + defer func() { + walletBlockAllowance = defaultWalletBlockAllowance + blockTicker = defaultBlockTicker + }() + + wallet, node, shutdown, _ := tNewWallet(true, walletTypeSPV) + defer shutdown() + + spv := wallet.node.(*spvWallet) + + getNote := func(timeout time.Duration) bool { + select { + case <-node.tipChanged: + return true + case <-time.After(timeout): + return false + } + } + + if getNote(walletBlockAllowance * 2) { + t.Fatalf("got a first block") + } + + var tipHeight int64 + addBlock := func() *block { + tipHeight++ + h, _ := node.addRawTx(tipHeight, dummyTx()) + return &block{tipHeight, *h} + } + + // Start with no blocks so that we're not synced. + node.getBlockchainInfo = &getBlockchainInfoResult{ + Headers: 2, + Blocks: 0, + } + + addBlock() + + // Prime the sync target to avoid the syncStatus tip send here. + atomic.StoreInt32(&spv.syncTarget, 1) + + // It should not come through on the block tick, since it will be cached. + if getNote(blockTicker * 2) { + t.Fatalf("got block that should've been cached") + } + + // And it won't come through after a sigle block allowance, because we're + // not synced. + if getNote(walletBlockAllowance * 2) { + t.Fatal("block didn't wait for the syncing mode allowance") + } + + // But it will come through after the sync timeout = 10 * normal timeout. + if !getNote(walletBlockAllowance * 9) { + t.Fatal("block didn't time out in syncing mode") + } + + // But if we're synced, it should come through after the normal block + // allowance. + addBlock() + node.blockchainMtx.Lock() + node.getBlockchainInfo = &getBlockchainInfoResult{ + Headers: tipHeight, + Blocks: tipHeight, + } + node.blockchainMtx.Unlock() + if !getNote(walletBlockAllowance * 2) { + t.Fatal("block didn't time out in normal mode") + } + + // On the other hand, a wallet block should come through immediately. Not + // even waiting on the block tick. + spv.tipChan <- addBlock() + if !getNote(blockTicker / 2) { + t.Fatal("wallet block wasn't sent through") + } + + // If we do the same thing but make sure that a polled block is queued + // first, we should still see the block right away, and the queued block + // should be canceled. + blk := addBlock() + time.Sleep(blockTicker * 2) + spv.tipChan <- blk + if !getNote(blockTicker / 2) { + t.Fatal("wallet block wasn't sent through with polled block queued") + } + + if getNote(walletBlockAllowance * 2) { + t.Fatal("queued polled block that should have been canceled came through") + } +} diff --git a/client/asset/btc/wallet.go b/client/asset/btc/wallet.go index fe7ccacda0..398b7572c3 100644 --- a/client/asset/btc/wallet.go +++ b/client/asset/btc/wallet.go @@ -37,10 +37,14 @@ type Wallet interface { locked() bool syncStatus() (*syncStatus, error) swapConfirmations(txHash *chainhash.Hash, vout uint32, contract []byte, startTime time.Time) (confs uint32, spent bool, err error) - getBlockHeader(blockHash string) (*blockHeader, error) + getBlockHeader(blockHash *chainhash.Hash) (*blockHeader, error) ownsAddress(addr btcutil.Address) (bool, error) getWalletTransaction(txHash *chainhash.Hash) (*GetTransactionResult, error) searchBlockForRedemptions(ctx context.Context, reqs map[outPoint]*findRedemptionReq, blockHash chainhash.Hash) (discovered map[outPoint]*findRedemptionResult) findRedemptionsInMempool(ctx context.Context, reqs map[outPoint]*findRedemptionReq) (discovered map[outPoint]*findRedemptionResult) getBlock(h chainhash.Hash) (*wire.MsgBlock, error) } + +type tipNotifier interface { + tipFeed() <-chan *block +} diff --git a/client/core/bookie.go b/client/core/bookie.go index 2e00af8360..8f272ac209 100644 --- a/client/core/bookie.go +++ b/client/core/bookie.go @@ -935,7 +935,7 @@ func (dc *dexConnection) subPriceFeed() { var msgErr *msgjson.Error // Ignore old servers' errors. if !errors.As(err, &msgErr) || msgErr.Code != msgjson.UnknownMessageType { - dc.log.Errorf("unable to fetch market overview: %w", err) + dc.log.Errorf("unable to fetch market overview: %v", err) } return } diff --git a/go.mod b/go.mod index 85a311faf2..ba4489cd12 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.16 require ( decred.org/dcrwallet/v2 v2.0.0-20210913145543-714c2f555f04 - github.com/btcsuite/btcd v0.22.0-beta.0.20210803133449-f5a1fb9965e4 + github.com/btcsuite/btcd v0.22.0-beta.0.20211026140004-31791ba4dc6e github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f github.com/btcsuite/btcutil v1.0.3-0.20210527170813-e2ba6805a890 // note: hoists btcd's own require of btcutil github.com/btcsuite/btcutil/psbt v1.0.3-0.20201208143702-a53e38424cce diff --git a/go.sum b/go.sum index a4b28568d2..b86a0ba2ba 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,9 @@ github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcug github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.21.0-beta.0.20201208033208-6bd4c64a54fa/go.mod h1:Sv4JPQ3/M+teHz9Bo5jBpkNcP0x6r7rdihlNL/7tTAs= github.com/btcsuite/btcd v0.21.0-beta.0.20210426180113-7eba688b65e5/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN0VB8L8svzOA= -github.com/btcsuite/btcd v0.22.0-beta.0.20210803133449-f5a1fb9965e4 h1:EmyLrldY44jDVa3dQ2iscj1S6ExuVJhRzCZBOXo93r0= github.com/btcsuite/btcd v0.22.0-beta.0.20210803133449-f5a1fb9965e4/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN0VB8L8svzOA= +github.com/btcsuite/btcd v0.22.0-beta.0.20211026140004-31791ba4dc6e h1:d0NkvbJGQThTkhypOdtf5Orxe2+dUHLB86pQ4EXwrTo= +github.com/btcsuite/btcd v0.22.0-beta.0.20211026140004-31791ba4dc6e/go.mod h1:3PH+KbvLFfzBTCevQenPiDedjGQGt6aa70dVjJDWGTA= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=