decred.org/dcrwallet/v3@v3.1.0/wallet/rescan.go (about) 1 // Copyright (c) 2013-2014 The btcsuite developers 2 // Copyright (c) 2015-2020 The Decred developers 3 // Use of this source code is governed by an ISC 4 // license that can be found in the LICENSE file. 5 6 package wallet 7 8 import ( 9 "context" 10 "time" 11 12 "decred.org/dcrwallet/v3/errors" 13 "decred.org/dcrwallet/v3/wallet/udb" 14 "decred.org/dcrwallet/v3/wallet/walletdb" 15 "github.com/decred/dcrd/chaincfg/chainhash" 16 "github.com/decred/dcrd/crypto/ripemd160" 17 "github.com/decred/dcrd/txscript/v4/stdaddr" 18 "github.com/decred/dcrd/wire" 19 ) 20 21 const maxBlocksPerRescan = 2000 22 23 // RescanFilter implements a precise filter intended to hold all watched wallet 24 // data in memory such as addresses and unspent outputs. The zero value is not 25 // valid, and filters must be created using NewRescanFilter. RescanFilter is 26 // not safe for concurrent access. 27 type RescanFilter struct { 28 // Implemented fast paths for address lookup. 29 pubKeyHashes map[[ripemd160.Size]byte]struct{} 30 scriptHashes map[[ripemd160.Size]byte]struct{} 31 compressedPubKeys map[[33]byte]struct{} 32 33 // A fallback address lookup map in case a fast path doesn't exist. 34 // Only exists for completeness. If using this shows up in a profile, 35 // there's a good chance a fast path should be added. 36 otherAddresses map[string]struct{} 37 38 // Outpoints of unspent outputs. 39 unspent map[wire.OutPoint]struct{} 40 } 41 42 // NewRescanFilter creates and initializes a RescanFilter containing each passed 43 // address and outpoint. 44 func NewRescanFilter(addresses []stdaddr.Address, unspentOutPoints []*wire.OutPoint) *RescanFilter { 45 filter := &RescanFilter{ 46 pubKeyHashes: map[[ripemd160.Size]byte]struct{}{}, 47 scriptHashes: map[[ripemd160.Size]byte]struct{}{}, 48 compressedPubKeys: map[[33]byte]struct{}{}, 49 otherAddresses: map[string]struct{}{}, 50 unspent: make(map[wire.OutPoint]struct{}, len(unspentOutPoints)), 51 } 52 53 for _, s := range addresses { 54 filter.AddAddress(s) 55 } 56 for _, op := range unspentOutPoints { 57 filter.AddUnspentOutPoint(op) 58 } 59 60 return filter 61 } 62 63 // AddAddress adds an address to the filter if it does not already exist. 64 func (f *RescanFilter) AddAddress(a stdaddr.Address) { 65 switch a := a.(type) { 66 case *stdaddr.AddressPubKeyHashEcdsaSecp256k1V0: 67 f.pubKeyHashes[*a.Hash160()] = struct{}{} 68 case *stdaddr.AddressScriptHashV0: 69 f.scriptHashes[*a.Hash160()] = struct{}{} 70 case *stdaddr.AddressPubKeyEcdsaSecp256k1V0: 71 serializedPubKey := a.SerializedPubKey() 72 switch len(serializedPubKey) { 73 case 33: // compressed 74 var compressedPubKey [33]byte 75 copy(compressedPubKey[:], serializedPubKey) 76 f.compressedPubKeys[compressedPubKey] = struct{}{} 77 default: 78 f.otherAddresses[a.String()] = struct{}{} 79 } 80 default: 81 f.otherAddresses[a.String()] = struct{}{} 82 } 83 } 84 85 // ExistsAddress returns whether an address is contained in the filter. 86 func (f *RescanFilter) ExistsAddress(a stdaddr.Address) (ok bool) { 87 switch a := a.(type) { 88 case *stdaddr.AddressPubKeyHashEcdsaSecp256k1V0: 89 _, ok = f.pubKeyHashes[*a.Hash160()] 90 case *stdaddr.AddressScriptHashV0: 91 _, ok = f.scriptHashes[*a.Hash160()] 92 case *stdaddr.AddressPubKeyEcdsaSecp256k1V0: 93 serializedPubKey := a.SerializedPubKey() 94 switch len(serializedPubKey) { 95 case 33: // compressed 96 var compressedPubKey [33]byte 97 copy(compressedPubKey[:], serializedPubKey) 98 _, ok = f.compressedPubKeys[compressedPubKey] 99 if !ok { 100 a := a.AddressPubKeyHash().(*stdaddr.AddressPubKeyHashEcdsaSecp256k1V0) 101 _, ok = f.pubKeyHashes[*a.Hash160()] 102 } 103 default: 104 _, ok = f.otherAddresses[a.String()] 105 } 106 default: 107 _, ok = f.otherAddresses[a.String()] 108 } 109 return 110 } 111 112 // RemoveAddress removes an address from the filter if it exists. 113 func (f *RescanFilter) RemoveAddress(a stdaddr.Address) { 114 switch a := a.(type) { 115 case *stdaddr.AddressPubKeyHashEcdsaSecp256k1V0: 116 delete(f.pubKeyHashes, *a.Hash160()) 117 case *stdaddr.AddressScriptHashV0: 118 delete(f.scriptHashes, *a.Hash160()) 119 case *stdaddr.AddressPubKeyEcdsaSecp256k1V0: 120 serializedPubKey := a.SerializedPubKey() 121 switch len(serializedPubKey) { 122 case 33: // compressed 123 var compressedPubKey [33]byte 124 copy(compressedPubKey[:], serializedPubKey) 125 delete(f.compressedPubKeys, compressedPubKey) 126 default: 127 delete(f.otherAddresses, a.String()) 128 } 129 default: 130 delete(f.otherAddresses, a.String()) 131 } 132 } 133 134 // AddUnspentOutPoint adds an outpoint to the filter if it does not already 135 // exist. 136 func (f *RescanFilter) AddUnspentOutPoint(op *wire.OutPoint) { 137 f.unspent[*op] = struct{}{} 138 } 139 140 // ExistsUnspentOutPoint returns whether an outpoint is contained in the filter. 141 func (f *RescanFilter) ExistsUnspentOutPoint(op *wire.OutPoint) bool { 142 _, ok := f.unspent[*op] 143 return ok 144 } 145 146 // RemoveUnspentOutPoint removes an outpoint from the filter if it exists. 147 func (f *RescanFilter) RemoveUnspentOutPoint(op *wire.OutPoint) { 148 delete(f.unspent, *op) 149 } 150 151 // saveRescanned records transactions from a rescanned block. This 152 // does not update the network backend with data to watch for future 153 // relevant transactions as the rescanner is assumed to handle this 154 // task. 155 func (w *Wallet) saveRescanned(ctx context.Context, hash *chainhash.Hash, txs []*wire.MsgTx) error { 156 const op errors.Op = "wallet.saveRescanned" 157 158 defer w.lockedOutpointMu.Unlock() 159 w.lockedOutpointMu.Lock() 160 161 err := walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error { 162 txmgrNs := dbtx.ReadWriteBucket(wtxmgrNamespaceKey) 163 blockMeta, err := w.txStore.GetBlockMetaForHash(txmgrNs, hash) 164 if err != nil { 165 return err 166 } 167 header, err := w.txStore.GetBlockHeader(dbtx, hash) 168 if err != nil { 169 return err 170 } 171 172 for _, tx := range txs { 173 rec, err := udb.NewTxRecordFromMsgTx(tx, time.Now()) 174 if err != nil { 175 return err 176 } 177 _, err = w.processTransactionRecord(ctx, dbtx, rec, header, &blockMeta) 178 if err != nil { 179 return err 180 } 181 } 182 return w.txStore.UpdateProcessedTxsBlockMarker(dbtx, hash) 183 }) 184 if err != nil { 185 return errors.E(op, err) 186 } 187 return nil 188 } 189 190 // rescan synchronously scans over all blocks on the main chain starting at 191 // startHash and height up through the recorded main chain tip block. The 192 // progress channel, if non-nil, is sent non-error progress notifications with 193 // the heights the rescan has completed through, starting with the start height. 194 func (w *Wallet) rescan(ctx context.Context, n NetworkBackend, 195 startHash *chainhash.Hash, height int32, p chan<- RescanProgress) error { 196 197 blockHashStorage := make([]chainhash.Hash, maxBlocksPerRescan) 198 rescanFrom := *startHash 199 inclusive := true 200 for { 201 select { 202 case <-ctx.Done(): 203 return ctx.Err() 204 default: 205 } 206 207 var rescanBlocks []chainhash.Hash 208 err := walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error { 209 txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) 210 var err error 211 rescanBlocks, err = w.txStore.GetMainChainBlockHashes(txmgrNs, 212 &rescanFrom, inclusive, blockHashStorage) 213 return err 214 }) 215 if err != nil { 216 return err 217 } 218 if len(rescanBlocks) == 0 { 219 break 220 } 221 222 through := height + int32(len(rescanBlocks)) - 1 223 // Genesis block is not rescanned 224 if height == 0 { 225 rescanBlocks = rescanBlocks[1:] 226 height = 1 227 if len(rescanBlocks) == 0 { 228 break 229 } 230 } 231 log.Infof("Rescanning block range [%v, %v]...", height, through) 232 saveRescanned := func(block *chainhash.Hash, txs []*wire.MsgTx) error { 233 return w.saveRescanned(ctx, block, txs) 234 } 235 err = n.Rescan(ctx, rescanBlocks, saveRescanned) 236 if err != nil { 237 return err 238 } 239 err = walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error { 240 return w.txStore.UpdateProcessedTxsBlockMarker(dbtx, &rescanBlocks[len(rescanBlocks)-1]) 241 }) 242 if err != nil { 243 return err 244 } 245 if p != nil { 246 p <- RescanProgress{ScannedThrough: through} 247 } 248 rescanFrom = rescanBlocks[len(rescanBlocks)-1] 249 height += int32(len(rescanBlocks)) 250 inclusive = false 251 } 252 253 log.Infof("Rescan complete") 254 return nil 255 } 256 257 // Rescan starts a rescan of the wallet for all blocks on the main chain 258 // beginning at startHash. This function blocks until the rescan completes. 259 func (w *Wallet) Rescan(ctx context.Context, n NetworkBackend, startHash *chainhash.Hash) error { 260 const op errors.Op = "wallet.Rescan" 261 262 var startHeight int32 263 err := walletdb.View(ctx, w.db, func(tx walletdb.ReadTx) error { 264 txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) 265 header, err := w.txStore.GetSerializedBlockHeader(txmgrNs, startHash) 266 if err != nil { 267 return err 268 } 269 startHeight = udb.ExtractBlockHeaderHeight(header) 270 return nil 271 }) 272 if err != nil { 273 return errors.E(op, err) 274 } 275 276 err = w.rescan(ctx, n, startHash, startHeight, nil) 277 if err != nil { 278 return errors.E(op, err) 279 } 280 return nil 281 } 282 283 // RescanFromHeight is an alternative to Rescan that takes a block height 284 // instead of a hash. See Rescan for more details. 285 func (w *Wallet) RescanFromHeight(ctx context.Context, n NetworkBackend, startHeight int32) error { 286 const op errors.Op = "wallet.RescanFromHeight" 287 288 var startHash chainhash.Hash 289 err := walletdb.View(ctx, w.db, func(tx walletdb.ReadTx) error { 290 txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) 291 var err error 292 startHash, err = w.txStore.GetMainChainBlockHashForHeight( 293 txmgrNs, startHeight) 294 return err 295 }) 296 if err != nil { 297 return errors.E(op, err) 298 } 299 300 err = w.rescan(ctx, n, &startHash, startHeight, nil) 301 if err != nil { 302 return errors.E(op, err) 303 } 304 return nil 305 } 306 307 // RescanProgress records the height the rescan has completed through and any 308 // errors during processing of the rescan. 309 type RescanProgress struct { 310 Err error 311 ScannedThrough int32 312 } 313 314 // RescanProgressFromHeight rescans for relevant transactions in all blocks in 315 // the main chain starting at startHeight. Progress notifications and any 316 // errors are sent to the channel p. This function blocks until the rescan 317 // completes or ends in an error. p is closed before returning. 318 func (w *Wallet) RescanProgressFromHeight(ctx context.Context, n NetworkBackend, 319 startHeight int32, p chan<- RescanProgress) { 320 321 const op errors.Op = "wallet.RescanProgressFromHeight" 322 323 defer close(p) 324 325 var startHash chainhash.Hash 326 err := walletdb.View(ctx, w.db, func(tx walletdb.ReadTx) error { 327 txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey) 328 var err error 329 startHash, err = w.txStore.GetMainChainBlockHashForHeight( 330 txmgrNs, startHeight) 331 return err 332 }) 333 if err != nil { 334 p <- RescanProgress{Err: errors.E(op, err)} 335 return 336 } 337 338 err = w.rescan(ctx, n, &startHash, startHeight, p) 339 if err != nil { 340 p <- RescanProgress{Err: errors.E(op, err)} 341 } 342 } 343 344 func (w *Wallet) mainChainAncestor(dbtx walletdb.ReadTx, hash *chainhash.Hash) (*chainhash.Hash, error) { 345 for { 346 mainChain, _ := w.txStore.BlockInMainChain(dbtx, hash) 347 if mainChain { 348 break 349 } 350 h, err := w.txStore.GetBlockHeader(dbtx, hash) 351 if err != nil { 352 return nil, err 353 } 354 hash = &h.PrevBlock 355 } 356 return hash, nil 357 } 358 359 // RescanPoint returns the block hash at which a rescan should begin 360 // (inclusive), or nil when no rescan is necessary. 361 func (w *Wallet) RescanPoint(ctx context.Context) (*chainhash.Hash, error) { 362 const op errors.Op = "wallet.RescanPoint" 363 var rp *chainhash.Hash 364 err := walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error { 365 var err error 366 rp, err = w.rescanPoint(dbtx) 367 return err 368 }) 369 if err != nil { 370 return nil, errors.E(op, err) 371 } 372 return rp, nil 373 } 374 375 func (w *Wallet) rescanPoint(dbtx walletdb.ReadTx) (*chainhash.Hash, error) { 376 ns := dbtx.ReadBucket(wtxmgrNamespaceKey) 377 r := w.txStore.ProcessedTxsBlockMarker(dbtx) 378 r, err := w.mainChainAncestor(dbtx, r) // Walk back to the main chain ancestor 379 if err != nil { 380 return nil, err 381 } 382 if tipHash, _ := w.txStore.MainChainTip(dbtx); *r == tipHash { 383 return nil, nil 384 } 385 // r is not the tip, so a child block must exist in the main chain. 386 h, err := w.txStore.GetBlockHeader(dbtx, r) 387 if err != nil { 388 log.Info(err) 389 return nil, err 390 } 391 rescanPoint, err := w.txStore.GetMainChainBlockHashForHeight(ns, int32(h.Height)+1) 392 if err != nil { 393 log.Info(err) 394 return nil, err 395 } 396 return &rescanPoint, nil 397 }