Skip to content

Commit 802a6b4

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 c968419 commit 802a6b4

8 files changed

Lines changed: 361 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"
@@ -223,6 +224,29 @@ func (s *Syncer) getHeaders(ctx context.Context) error {
223224
return err
224225
}
225226

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

228252
cnet := s.wallet.ChainParams().Net
@@ -255,15 +279,22 @@ func (s *Syncer) getHeaders(ctx context.Context) error {
255279
g.Go(func() error {
256280
header := headers[i]
257281
hash := header.BlockHash()
258-
filter, proofIndex, proof, err := s.rpc.CFilterV2(ctx, &hash)
259-
if err != nil {
260-
return err
261-
}
262-
263-
err = validate.CFilterV2HeaderCommitment(cnet, header,
264-
filter, proofIndex, proof)
265-
if err != nil {
266-
return err
282+
var filter *gcs.FilterV2
283+
if afterBirthday(header) {
284+
var (
285+
proofIndex uint32
286+
proof []chainhash.Hash
287+
)
288+
filter, proofIndex, proof, err = s.rpc.CFilterV2(ctx, &hash)
289+
if err != nil {
290+
return err
291+
}
292+
293+
err = validate.CFilterV2HeaderCommitment(cnet, header,
294+
filter, proofIndex, proof)
295+
if err != nil {
296+
return err
297+
}
267298
}
268299

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

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

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

11961202
**Request:** `ImportVotingAccountFromSeedRequest`
11971203

@@ -2690,7 +2696,10 @@ or account must be unlocked.
26902696
#### `BirthBlock`
26912697

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

26952704
**Request:** `BirthBlockRequest`
26962705

spv/sync.go

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

1665+
birthday, err := s.wallet.BirthState(ctx)
1666+
if err != nil {
1667+
return err
1668+
}
1669+
// If a birthday is set, it will likely not be found yet. Watch for it
1670+
// and do not download cfilters before it.
1671+
var afterBirthday func(h *wire.BlockHeader) bool
1672+
switch {
1673+
case birthday == nil:
1674+
afterBirthday = func(*wire.BlockHeader) bool {
1675+
return true
1676+
}
1677+
case birthday.SetFromTime:
1678+
afterBirthday = func(h *wire.BlockHeader) bool {
1679+
return h.Timestamp.After(birthday.Time)
1680+
}
1681+
default:
1682+
// Birthday is setting from height or was already found.
1683+
afterBirthday = func(h *wire.BlockHeader) bool {
1684+
return h.Height >= birthday.Height
1685+
}
1686+
}
1687+
16651688
// Stage 1: fetch headers.
16661689
headersChan := make(chan *headersBatch)
16671690
g.Go(func() error {
@@ -1737,6 +1760,9 @@ func (s *Syncer) initialSyncHeaders(ctx context.Context) error {
17371760
s.sidechainMu.Lock()
17381761
var missingCfilter []*wallet.BlockNode
17391762
for i := range batch.bestChain {
1763+
if !afterBirthday(batch.bestChain[i].Header) {
1764+
continue
1765+
}
17401766
if batch.bestChain[i].FilterV2 == nil {
17411767
missingCfilter = batch.bestChain[i:]
17421768
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)