decred.org/dcrwallet/v3@v3.1.0/spv/backend.go (about) 1 // Copyright (c) 2018-2019 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package spv 6 7 import ( 8 "context" 9 "runtime" 10 "sync" 11 12 "decred.org/dcrwallet/v3/errors" 13 "decred.org/dcrwallet/v3/p2p" 14 "decred.org/dcrwallet/v3/validate" 15 "decred.org/dcrwallet/v3/wallet" 16 "github.com/decred/dcrd/chaincfg/chainhash" 17 "github.com/decred/dcrd/dcrutil/v4" 18 "github.com/decred/dcrd/gcs/v4" 19 "github.com/decred/dcrd/txscript/v4/stdaddr" 20 "github.com/decred/dcrd/wire" 21 ) 22 23 var _ wallet.NetworkBackend = (*Syncer)(nil) 24 25 // TODO: When using the Syncer as a NetworkBackend, keep track of in-flight 26 // blocks and cfilters. If one is already incoming, wait on that response. If 27 // that peer is lost, try a different peer. Optionally keep a cache of fetched 28 // data so it can be immediately returned without another call. 29 30 func pickAny(*p2p.RemotePeer) bool { return true } 31 32 // Blocks implements the Blocks method of the wallet.Peer interface. 33 func (s *Syncer) Blocks(ctx context.Context, blockHashes []*chainhash.Hash) ([]*wire.MsgBlock, error) { 34 for { 35 if err := ctx.Err(); err != nil { 36 return nil, err 37 } 38 rp, err := s.pickRemote(pickAny) 39 if err != nil { 40 return nil, err 41 } 42 blocks, err := rp.Blocks(ctx, blockHashes) 43 if err != nil { 44 continue 45 } 46 return blocks, nil 47 } 48 } 49 50 // filterProof is an alias to the same anonymous struct as wallet package's 51 // FilterProof struct. 52 type filterProof = struct { 53 Filter *gcs.FilterV2 54 ProofIndex uint32 55 Proof []chainhash.Hash 56 } 57 58 // CFiltersV2 implements the CFiltersV2 method of the wallet.Peer interface. 59 func (s *Syncer) CFiltersV2(ctx context.Context, blockHashes []*chainhash.Hash) ([]filterProof, error) { 60 for { 61 if err := ctx.Err(); err != nil { 62 return nil, err 63 } 64 rp, err := s.pickRemote(pickAny) 65 if err != nil { 66 return nil, err 67 } 68 fs, err := rp.CFiltersV2(ctx, blockHashes) 69 if err != nil { 70 continue 71 } 72 return fs, nil 73 } 74 } 75 76 // Headers implements the Headers method of the wallet.Peer interface. 77 func (s *Syncer) Headers(ctx context.Context, blockLocators []*chainhash.Hash, hashStop *chainhash.Hash) ([]*wire.BlockHeader, error) { 78 for { 79 if err := ctx.Err(); err != nil { 80 return nil, err 81 } 82 rp, err := s.pickRemote(pickAny) 83 if err != nil { 84 return nil, err 85 } 86 hs, err := rp.Headers(ctx, blockLocators, hashStop) 87 if err != nil { 88 continue 89 } 90 return hs, nil 91 } 92 } 93 94 func (s *Syncer) String() string { 95 // This method is part of the wallet.Peer interface and will typically 96 // specify the remote address of the peer. Since the syncer can encompass 97 // multiple peers, just use the qualified type as the string. 98 return "spv.Syncer" 99 } 100 101 // LoadTxFilter implements the LoadTxFilter method of the wallet.NetworkBackend 102 // interface. 103 // 104 // NOTE: due to blockcf2 *not* including the spent outpoints in the block, the 105 // addrs[] slice MUST include the addresses corresponding to the respective 106 // outpoints, otherwise they will not be returned during the rescan. 107 func (s *Syncer) LoadTxFilter(ctx context.Context, reload bool, addrs []stdaddr.Address, outpoints []wire.OutPoint) error { 108 s.filterMu.Lock() 109 if reload || s.rescanFilter == nil { 110 s.rescanFilter = wallet.NewRescanFilter(nil, nil) 111 s.filterData = nil 112 } 113 for _, addr := range addrs { 114 _, pkScript := addr.PaymentScript() 115 s.rescanFilter.AddAddress(addr) 116 s.filterData.AddRegularPkScript(pkScript) 117 } 118 for i := range outpoints { 119 s.rescanFilter.AddUnspentOutPoint(&outpoints[i]) 120 } 121 s.filterMu.Unlock() 122 return nil 123 } 124 125 // PublishTransactions implements the PublishTransaction method of the 126 // wallet.Peer interface. 127 func (s *Syncer) PublishTransactions(ctx context.Context, txs ...*wire.MsgTx) error { 128 // Figure out transactions that are not stored by the wallet and create 129 // an aux map so we can choose which need to be stored in the syncer's 130 // mempool. 131 walletBacked := make(map[chainhash.Hash]bool, len(txs)) 132 relevant, _, err := s.wallet.DetermineRelevantTxs(ctx, txs...) 133 if err != nil { 134 return err 135 } 136 for _, tx := range relevant { 137 walletBacked[tx.TxHash()] = true 138 } 139 140 msg := wire.NewMsgInvSizeHint(uint(len(txs))) 141 for _, tx := range txs { 142 txHash := tx.TxHash() 143 if !walletBacked[txHash] { 144 // Load into the mempool and let the mempool handler 145 // know of it. 146 if _, loaded := s.mempool.LoadOrStore(txHash, tx); !loaded { 147 select { 148 case s.mempoolAdds <- &txHash: 149 case <-ctx.Done(): 150 return ctx.Err() 151 } 152 } 153 } 154 err := msg.AddInvVect(wire.NewInvVect(wire.InvTypeTx, &txHash)) 155 if err != nil { 156 return errors.E(errors.Protocol, err) 157 } 158 } 159 return s.forRemotes(func(rp *p2p.RemotePeer) error { 160 for _, inv := range msg.InvList { 161 rp.InvsSent().Add(inv.Hash) 162 } 163 return rp.SendMessage(ctx, msg) 164 }) 165 } 166 167 // Rescan implements the Rescan method of the wallet.NetworkBackend interface. 168 func (s *Syncer) Rescan(ctx context.Context, blockHashes []chainhash.Hash, save func(*chainhash.Hash, []*wire.MsgTx) error) error { 169 const op errors.Op = "spv.Rescan" 170 171 cfilters := make([]*gcs.FilterV2, 0, len(blockHashes)) 172 cfilterKeys := make([][gcs.KeySize]byte, 0, len(blockHashes)) 173 for i := 0; i < len(blockHashes); i++ { 174 k, f, err := s.wallet.CFilterV2(ctx, &blockHashes[i]) 175 if err != nil { 176 return err 177 } 178 cfilters = append(cfilters, f) 179 cfilterKeys = append(cfilterKeys, k) 180 } 181 182 blockMatches := make([]*wire.MsgBlock, len(blockHashes)) // Block assigned to slice once fetched 183 184 // Read current filter data. filterData is reassinged to new data matches 185 // for subsequent filter checks, which improves filter matching performance 186 // by checking for less data. 187 s.filterMu.Lock() 188 filterData := s.filterData 189 s.filterMu.Unlock() 190 191 idx := 0 192 FilterLoop: 193 for idx < len(blockHashes) { 194 var fmatches []*chainhash.Hash 195 var fmatchidx []int 196 var fmatchMu sync.Mutex 197 198 // Spawn ncpu workers to check filter matches 199 ncpu := runtime.NumCPU() 200 c := make(chan int, ncpu) 201 var wg sync.WaitGroup 202 wg.Add(ncpu) 203 for i := 0; i < ncpu; i++ { 204 go func() { 205 for i := range c { 206 blockHash := &blockHashes[i] 207 key := cfilterKeys[i] 208 f := cfilters[i] 209 if f.MatchAny(key, filterData) { 210 fmatchMu.Lock() 211 fmatches = append(fmatches, blockHash) 212 fmatchidx = append(fmatchidx, i) 213 fmatchMu.Unlock() 214 } 215 } 216 wg.Done() 217 }() 218 } 219 for i := idx; i < len(blockHashes); i++ { 220 if blockMatches[i] != nil { 221 // Already fetched this block 222 continue 223 } 224 c <- i 225 } 226 close(c) 227 wg.Wait() 228 229 if len(fmatches) != 0 { 230 var rp *p2p.RemotePeer 231 PickPeer: 232 for { 233 if err := ctx.Err(); err != nil { 234 return err 235 } 236 if rp == nil { 237 var err error 238 rp, err = s.pickRemote(pickAny) 239 if err != nil { 240 return err 241 } 242 } 243 244 blocks, err := rp.Blocks(ctx, fmatches) 245 if err != nil { 246 rp = nil 247 continue PickPeer 248 } 249 250 for j, b := range blocks { 251 // Validate fetched blocks before rescanning transactions. PoW 252 // and PoS difficulties have already been validated since the 253 // header is saved by the wallet, and modifications to these in 254 // the downloaded block would result in a different block hash 255 // and failure to fetch the block. 256 // 257 // Block filters were also validated 258 // against the header (assuming dcp0005 259 // was activated). 260 err = validate.MerkleRoots(b) 261 if err != nil { 262 err = validate.DCP0005MerkleRoot(b) 263 } 264 if err != nil { 265 err := errors.E(op, err) 266 rp.Disconnect(err) 267 rp = nil 268 continue PickPeer 269 } 270 271 i := fmatchidx[j] 272 blockMatches[i] = b 273 } 274 break 275 } 276 } 277 278 for i := idx; i < len(blockMatches); i++ { 279 b := blockMatches[i] 280 if b == nil { 281 // No filter match, skip block 282 continue 283 } 284 285 if err := ctx.Err(); err != nil { 286 return err 287 } 288 289 matchedTxs, fadded := s.rescanBlock(b) 290 if len(matchedTxs) != 0 { 291 err := save(&blockHashes[i], matchedTxs) 292 if err != nil { 293 return err 294 } 295 296 // Check for more matched blocks using updated filters, 297 // starting at the next block. 298 if len(fadded) != 0 { 299 idx = i + 1 300 filterData = fadded 301 continue FilterLoop 302 } 303 } 304 } 305 return nil 306 } 307 308 return nil 309 } 310 311 // StakeDifficulty implements the StakeDifficulty method of the 312 // wallet.NetworkBackend interface. 313 // 314 // This implementation of the method will always error as the stake difficulty 315 // is not queryable over wire protocol, and when the next stake difficulty is 316 // available in a header commitment, the wallet will be able to determine this 317 // itself without requiring the NetworkBackend. 318 func (s *Syncer) StakeDifficulty(ctx context.Context) (dcrutil.Amount, error) { 319 return 0, errors.E(errors.Invalid, "stake difficulty is not queryable over wire protocol") 320 }