Skip to content

Commit e80e4d6

Browse files
committed
wallet: Do not download cfilters before birthday.
Especially for spv wallets with a birthday, cfilters for blocks before the birthday are not used for anything as there can be no transactions before the wallet existed. Do not download them but check if we need them whenever doing a rescan from height.
1 parent 0d05c58 commit e80e4d6

8 files changed

Lines changed: 364 additions & 59 deletions

File tree

chain/sync.go

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/decred/dcrd/blockchain/stake/v5"
2525
"github.com/decred/dcrd/chaincfg/chainhash"
2626
"github.com/decred/dcrd/crypto/blake256"
27+
"github.com/decred/dcrd/gcs/v4"
2728
"github.com/decred/dcrd/mixing/mixpool"
2829
"github.com/decred/dcrd/wire"
2930
"github.com/jrick/wsrpc/v2"
@@ -228,6 +229,29 @@ func (s *Syncer) getHeaders(ctx context.Context) error {
228229
return err
229230
}
230231

232+
birthday, err := s.wallet.BirthState(ctx)
233+
if err != nil {
234+
return err
235+
}
236+
// If a birthday is set, it will likely not be found yet. Watch for it
237+
// and do not download cfilters before it.
238+
var afterBirthday func(h *wire.BlockHeader) bool
239+
switch {
240+
case birthday == nil:
241+
afterBirthday = func(*wire.BlockHeader) bool {
242+
return true
243+
}
244+
case birthday.SetFromTime:
245+
afterBirthday = func(h *wire.BlockHeader) bool {
246+
return h.Timestamp.After(birthday.Time)
247+
}
248+
default:
249+
// Birthday is setting from height or was already found.
250+
afterBirthday = func(h *wire.BlockHeader) bool {
251+
return h.Height >= birthday.Height
252+
}
253+
}
254+
231255
startedSynced := s.atomicWalletSynced.Load() == 1
232256

233257
cnet := s.wallet.ChainParams().Net
@@ -261,15 +285,22 @@ func (s *Syncer) getHeaders(ctx context.Context) error {
261285
g.Go(func() error {
262286
header := headers[i]
263287
hash := header.BlockHash()
264-
filter, proofIndex, proof, err := s.rpc.CFilterV2(ctx, &hash)
265-
if err != nil {
266-
return err
267-
}
268-
269-
err = validate.CFilterV2HeaderCommitment(cnet, header,
270-
filter, proofIndex, proof)
271-
if err != nil {
272-
return err
288+
var filter *gcs.FilterV2
289+
if afterBirthday(header) {
290+
var (
291+
proofIndex uint32
292+
proof []chainhash.Hash
293+
)
294+
filter, proofIndex, proof, err = s.rpc.CFilterV2(ctx, &hash)
295+
if err != nil {
296+
return err
297+
}
298+
299+
err = validate.CFilterV2HeaderCommitment(cnet, header,
300+
filter, proofIndex, proof)
301+
if err != nil {
302+
return err
303+
}
273304
}
274305

275306
nodes[i] = wallet.NewBlockNode(header, &hash, filter)

rpc/documentation/api.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,8 @@ ___
11091109
The `ImportPrivateKey` method imports a private key in Wallet Import Format
11101110
(WIF) encoding to a wallet account. A rescan may optionally be started to
11111111
search for transactions involving the private key's associated payment address.
1112+
If the private key deals with transactions before the wallet birthday, if set,
1113+
a rescan must be performed to download missing cfilters.
11121114

11131115
**Request:** `ImportPrivateKeyRequest`
11141116

@@ -1144,7 +1146,9 @@ ___
11441146

11451147
The `ImportScript` method imports a script into the wallet. A rescan may
11461148
optionally be started to search for transactions involving the script, either
1147-
as an output or in a P2SH input.
1149+
as an output or in a P2SH input. If the script deals with transactions before
1150+
the wallet birthday, if set, a rescan must be performed to download missing
1151+
cfilters.
11481152

11491153
**Request:** `ImportScriptRequest`
11501154

@@ -1190,7 +1194,9 @@ seed for a hierarchical deterministic private key that is imported into the
11901194
wallet with the supplied name and locked with the supplied password. Addresses
11911195
derived from this account MUST NOT be sent any funds. They are solely for the
11921196
use of creating stake submission scripts. A rescan may optionally be started to
1193-
search for tickets using submission scripts derived from this account.
1197+
search for tickets using submission scripts derived from this account. If tickets
1198+
would exist before the wallet birthday, if set, a rescan must be performed to
1199+
download missing cfilters.
11941200

11951201
**Request:** `ImportVotingAccountFromSeedRequest`
11961202

@@ -2689,7 +2695,10 @@ or account must be unlocked.
26892695
#### `BirthBlock`
26902696

26912697
The `BirthBlock` method returns the wallets birthday block if set. Rescans
2692-
should generally be started from after this block.
2698+
should generally be started from after this block. If a birthday is set cfilters
2699+
from before the birthday may not be downloaded. A rescan from height will move
2700+
the birthday to the rescan height and download all missing cfilters from that
2701+
height.
26932702

26942703
**Request:** `BirthBlockRequest`
26952704

spv/sync.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1660,6 +1660,29 @@ func (s *Syncer) initialSyncHeaders(ctx context.Context) error {
16601660
return res
16611661
}
16621662

1663+
birthday, err := s.wallet.BirthState(ctx)
1664+
if err != nil {
1665+
return err
1666+
}
1667+
// If a birthday is set, it will likely not be found yet. Watch for it
1668+
// and do not download cfilters before it.
1669+
var afterBirthday func(h *wire.BlockHeader) bool
1670+
switch {
1671+
case birthday == nil:
1672+
afterBirthday = func(*wire.BlockHeader) bool {
1673+
return true
1674+
}
1675+
case birthday.SetFromTime:
1676+
afterBirthday = func(h *wire.BlockHeader) bool {
1677+
return h.Timestamp.After(birthday.Time)
1678+
}
1679+
default:
1680+
// Birthday is setting from height or was already found.
1681+
afterBirthday = func(h *wire.BlockHeader) bool {
1682+
return h.Height >= birthday.Height
1683+
}
1684+
}
1685+
16631686
// Stage 1: fetch headers.
16641687
headersChan := make(chan *headersBatch)
16651688
g.Go(func() error {
@@ -1735,6 +1758,9 @@ func (s *Syncer) initialSyncHeaders(ctx context.Context) error {
17351758
s.sidechainMu.Lock()
17361759
var missingCfilter []*wallet.BlockNode
17371760
for i := range batch.bestChain {
1761+
if !afterBirthday(batch.bestChain[i].Header) {
1762+
continue
1763+
}
17381764
if batch.bestChain[i].FilterV2 == nil {
17391765
missingCfilter = batch.bestChain[i:]
17401766
break

wallet/rescan.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,8 +386,47 @@ func (w *Wallet) Rescan(ctx context.Context, n NetworkBackend, startHash *chainh
386386
func (w *Wallet) RescanFromHeight(ctx context.Context, n NetworkBackend, startHeight int32) error {
387387
const op errors.Op = "wallet.RescanFromHeight"
388388

389+
bs, err := w.BirthState(ctx)
390+
if err != nil {
391+
return errors.E(op, err)
392+
}
393+
if bs != nil && int32(bs.Height) > startHeight {
394+
// If our birthday is before the rescan height, we may
395+
// not have the cfilters needed. Set birthday to the rescan
396+
// height and download the filters. This may take some time
397+
// depending on network conditions and amount of filters missing.
398+
bs := &udb.BirthdayState{
399+
SetFromHeight: true,
400+
Height: uint32(startHeight),
401+
}
402+
if err := w.SetBirthStateAndScan(ctx, bs); err != nil {
403+
return errors.E(op, err)
404+
}
405+
fetchMissing := true
406+
if err := walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error {
407+
if _, err := udb.MissingCFiltersHeight(dbtx, startHeight); err != nil {
408+
// errors.NotExist is returned if we already have all filters
409+
// from start height. If we have them there is no need to
410+
// fetch them again.
411+
if errors.Is(err, errors.NotExist) {
412+
fetchMissing = false
413+
return nil
414+
}
415+
return errors.E(op, err)
416+
}
417+
return w.txStore.SetMissingMainChainCFilters(dbtx, false)
418+
}); err != nil {
419+
return errors.E(op, err)
420+
}
421+
if fetchMissing {
422+
if err := w.FetchMissingCFilters(ctx, n); err != nil {
423+
return errors.E(op, err)
424+
}
425+
}
426+
}
427+
389428
var startHash chainhash.Hash
390-
err := walletdb.View(ctx, w.db, func(tx walletdb.ReadTx) error {
429+
err = walletdb.View(ctx, w.db, func(tx walletdb.ReadTx) error {
391430
txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey)
392431
var err error
393432
startHash, err = w.txStore.GetMainChainBlockHashForHeight(

wallet/udb/txmined.go

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,8 @@ func (s *Store) MainChainTip(dbtx walletdb.ReadTx) (chainhash.Hash, int32) {
198198
// If the block is already inserted and part of the main chain, an errors.Exist
199199
// error is returned.
200200
//
201-
// The main chain tip may not be extended unless compact filters have been saved
202-
// for all existing main chain blocks.
201+
// The main chain may be extended without cfilters if this block is before the
202+
// wallet birthday. If the filter is nil it will not be saved to the database.
203203
func (s *Store) ExtendMainChain(ns walletdb.ReadWriteBucket, header *wire.BlockHeader, blockHash *chainhash.Hash, f *gcs2.FilterV2) error {
204204
height := int32(header.Height)
205205
if height < 1 {
@@ -266,9 +266,12 @@ func (s *Store) ExtendMainChain(ns walletdb.ReadWriteBucket, header *wire.BlockH
266266
return err
267267
}
268268

269-
// Save the compact filter.
270-
bcf2Key := blockcf2.Key(&header.MerkleRoot)
271-
return putRawCFilter(ns, blockHash[:], valueRawCFilter2(bcf2Key, f.Bytes()))
269+
// Save the compact filter if we have it.
270+
if f != nil {
271+
bcf2Key := blockcf2.Key(&header.MerkleRoot)
272+
return putRawCFilter(ns, blockHash[:], valueRawCFilter2(bcf2Key, f.Bytes()))
273+
}
274+
return nil
272275
}
273276

274277
// ProcessedTxsBlockMarker returns the hash of the block which records the last
@@ -402,19 +405,37 @@ func (s *Store) IsMissingMainChainCFilters(dbtx walletdb.ReadTx) bool {
402405
return len(v) != 1 || v[0] == 0
403406
}
404407

408+
// SetMissingMainChainCFilters sets whether we have all of the main chain
409+
// cfilters. Should be used to set missing to false if the wallet birthday is
410+
// moved back in time.
411+
func (s *Store) SetMissingMainChainCFilters(dbtx walletdb.ReadWriteTx, have bool) error {
412+
haveB := []byte{0}
413+
if have {
414+
haveB = []byte{1}
415+
}
416+
err := dbtx.ReadWriteBucket(wtxmgrBucketKey).Put(rootHaveCFilters, haveB)
417+
if err != nil {
418+
return errors.E(errors.IO, err)
419+
}
420+
return nil
421+
}
422+
405423
// MissingCFiltersHeight returns the first main chain block height
406424
// with a missing cfilter. Errors with NotExist when all main chain
407425
// blocks record cfilters.
408-
func (s *Store) MissingCFiltersHeight(dbtx walletdb.ReadTx) (int32, error) {
426+
func MissingCFiltersHeight(dbtx walletdb.ReadTx, fromHeight int32) (int32, error) {
409427
ns := dbtx.ReadBucket(wtxmgrBucketKey)
410428
c := ns.NestedReadBucket(bucketBlocks).ReadCursor()
411429
defer c.Close()
412-
for k, v := c.First(); k != nil; k, v = c.Next() {
430+
for k, v := c.Seek(keyBlockRecord(fromHeight)); k != nil; k, v = c.Next() {
413431
hash := extractRawBlockRecordHash(v)
414432
_, _, err := fetchRawCFilter2(ns, hash)
415-
if errors.Is(err, errors.NotExist) {
416-
height := int32(byteOrder.Uint32(k))
417-
return height, nil
433+
if err != nil {
434+
if errors.Is(err, errors.NotExist) {
435+
height := int32(byteOrder.Uint32(k))
436+
return height, nil
437+
}
438+
return 0, errors.E(errors.IO, err)
418439
}
419440
}
420441
return 0, errors.E(errors.NotExist)
@@ -442,42 +463,37 @@ func (s *Store) InsertMissingCFilters(dbtx walletdb.ReadWriteTx, blockHashes []*
442463
}
443464

444465
for i, blockHash := range blockHashes {
445-
// Ensure that blockHashes are ordered and that all previous cfilters in the
446-
// main chain are known.
466+
// Ensure that blockHashes are ordered.
467+
header := existsBlockHeader(ns, blockHash[:])
468+
if header == nil {
469+
return errors.E(errors.NotExist, errors.Errorf("missing header for block %v", blockHash))
470+
}
447471
ok := i == 0 && *blockHash == s.chainParams.GenesisHash
448-
var bcf2Key [gcs2.KeySize]byte
449472
if !ok {
450-
header := existsBlockHeader(ns, blockHash[:])
451-
if header == nil {
452-
return errors.E(errors.NotExist, errors.Errorf("missing header for block %v", blockHash))
453-
}
454473
parentHash := extractBlockHeaderParentHash(header)
455-
merkleRoot := extractBlockHeaderMerkleRoot(header)
456-
merkleRootHash, err := chainhash.NewHash(merkleRoot)
457-
if err != nil {
458-
return errors.E(errors.Invalid, errors.Errorf("invalid stored header %v", blockHash))
459-
}
460-
bcf2Key = blockcf2.Key(merkleRootHash)
461-
if i == 0 {
462-
_, _, err := fetchRawCFilter2(ns, parentHash)
463-
ok = err == nil
464-
} else {
465-
ok = bytes.Equal(parentHash, blockHashes[i-1][:])
474+
if i != 0 {
475+
if !bytes.Equal(parentHash, blockHashes[i-1][:]) {
476+
return errors.E(errors.Invalid, "block hashes are not ordered")
477+
}
466478
}
467479
}
468-
if !ok {
469-
return errors.E(errors.Invalid, "block hashes are not ordered or previous cfilters are missing")
470-
}
471480

472481
// Record cfilter for this block
473-
err := putRawCFilter(ns, blockHash[:], valueRawCFilter2(bcf2Key, filters[i].Bytes()))
482+
merkleRoot := extractBlockHeaderMerkleRoot(header)
483+
merkleRootHash, err := chainhash.NewHash(merkleRoot)
484+
if err != nil {
485+
return errors.E(errors.Invalid, errors.Errorf("invalid stored header %v", blockHash))
486+
}
487+
bcf2Key := blockcf2.Key(merkleRootHash)
488+
err = putRawCFilter(ns, blockHash[:], valueRawCFilter2(bcf2Key, filters[i].Bytes()))
474489
if err != nil {
475490
return err
476491
}
477492
}
478493

479494
// Mark all main chain cfilters as saved if the last block hash is the main
480-
// chain tip.
495+
// chain tip. Even if this is not the head block, all cfilters may be saved
496+
// at this point. The caller may need to check and set rootHaveCFilters.
481497
tip, _ := s.MainChainTip(dbtx)
482498
if bytes.Equal(tip[:], blockHashes[len(blockHashes)-1][:]) {
483499
err := ns.Put(rootHaveCFilters, []byte{1})

0 commit comments

Comments
 (0)