Skip to content

Commit 092584c

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 667600d commit 092584c

8 files changed

Lines changed: 307 additions & 21 deletions

File tree

chain/sync.go

Lines changed: 41 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,30 @@ 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 was set and already found. Can happen if sync was
250+
// stopped and started again after the birthday was found.
251+
afterBirthday = func(h *wire.BlockHeader) bool {
252+
return h.Height >= birthday.Height
253+
}
254+
}
255+
231256
startedSynced := s.atomicWalletSynced.Load() == 1
232257

233258
cnet := s.wallet.ChainParams().Net
@@ -261,15 +286,22 @@ func (s *Syncer) getHeaders(ctx context.Context) error {
261286
g.Go(func() error {
262287
header := headers[i]
263288
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
289+
var filter *gcs.FilterV2
290+
if afterBirthday(header) {
291+
var (
292+
proofIndex uint32
293+
proof []chainhash.Hash
294+
)
295+
filter, proofIndex, proof, err = s.rpc.CFilterV2(ctx, &hash)
296+
if err != nil {
297+
return err
298+
}
299+
300+
err = validate.CFilterV2HeaderCommitment(cnet, header,
301+
filter, proofIndex, proof)
302+
if err != nil {
303+
return err
304+
}
273305
}
274306

275307
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: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1660,6 +1660,30 @@ 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 was set and already found. Can happen if sync was
1681+
// stopped and started again after the birthday was found.
1682+
afterBirthday = func(h *wire.BlockHeader) bool {
1683+
return h.Height >= birthday.Height
1684+
}
1685+
}
1686+
16631687
// Stage 1: fetch headers.
16641688
headersChan := make(chan *headersBatch)
16651689
g.Go(func() error {
@@ -1735,6 +1759,9 @@ func (s *Syncer) initialSyncHeaders(ctx context.Context) error {
17351759
s.sidechainMu.Lock()
17361760
var missingCfilter []*wallet.BlockNode
17371761
for i := range batch.bestChain {
1762+
if !afterBirthday(batch.bestChain[i].Header) {
1763+
continue
1764+
}
17381765
if batch.bestChain[i].FilterV2 == nil {
17391766
missingCfilter = batch.bestChain[i:]
17401767
break

wallet/rescan.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,8 +386,36 @@ 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 {
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+
if int32(bs.Height) > startHeight {
399+
bs := &udb.BirthdayState{
400+
SetFromHeight: true,
401+
Height: uint32(startHeight),
402+
}
403+
if err := w.SetBirthStateAndScan(ctx, bs); err != nil {
404+
return errors.E(op, err)
405+
}
406+
if err := walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error {
407+
return w.txStore.SetMissingMainChainCFilters(dbtx, true)
408+
}); err != nil {
409+
return errors.E(op, err)
410+
}
411+
if err := w.FetchMissingCFilters(ctx, n); err != nil {
412+
return errors.E(op, err)
413+
}
414+
}
415+
}
416+
389417
var startHash chainhash.Hash
390-
err := walletdb.View(ctx, w.db, func(tx walletdb.ReadTx) error {
418+
err = walletdb.View(ctx, w.db, func(tx walletdb.ReadTx) error {
391419
txmgrNs := tx.ReadBucket(wtxmgrNamespaceKey)
392420
var err error
393421
startHash, err = w.txStore.GetMainChainBlockHashForHeight(

wallet/udb/txmined.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
if f != nil {
270+
// Save the compact filter.
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,14 +405,29 @@ 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)
415433
if errors.Is(err, errors.NotExist) {

wallet/udb/txmined_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"decred.org/dcrwallet/v5/wallet/walletdb"
1313
"github.com/decred/dcrd/chaincfg/chainhash"
1414
"github.com/decred/dcrd/crypto/rand"
15+
"github.com/decred/dcrd/dcrutil/v4"
16+
gcs2 "github.com/decred/dcrd/gcs/v4"
1517
)
1618

1719
func randomBytes(len int) []byte {
@@ -79,3 +81,108 @@ func TestSetBirthState(t *testing.T) {
7981
})
8082
}
8183
}
84+
85+
func TestMissingCFiltersHeight(t *testing.T) {
86+
ctx := context.Background()
87+
db, _, s, teardown, err := cloneDB(ctx, "mgr_watching_only.kv")
88+
defer teardown()
89+
if err != nil {
90+
t.Fatal(err)
91+
}
92+
93+
g := makeBlockGenerator()
94+
b1H := g.generate(dcrutil.BlockValid)
95+
b2H := g.generate(dcrutil.BlockValid)
96+
b3H := g.generate(dcrutil.BlockValid)
97+
b4H := g.generate(dcrutil.BlockValid)
98+
b5H := g.generate(dcrutil.BlockValid)
99+
headerData := makeHeaderDataSlice(b1H, b2H, b3H, b4H, b5H)
100+
// 3 with filters including genesis then two without and last one with.
101+
filters := emptyFilters(2)
102+
filters = append(filters, make([]*gcs2.FilterV2, 2)...)
103+
filters = append(filters, emptyFilters(1)...)
104+
105+
err = walletdb.Update(ctx, db, func(dbtx walletdb.ReadWriteTx) error {
106+
err = insertMainChainHeaders(s, dbtx, headerData, filters)
107+
if err != nil {
108+
return err
109+
}
110+
return nil
111+
})
112+
if err != nil {
113+
t.Fatal(err)
114+
}
115+
116+
tests := []struct {
117+
name string
118+
missingNo, from int32
119+
wantErr bool
120+
do func()
121+
}{{
122+
name: "ok from 0",
123+
missingNo: 3,
124+
}, {
125+
name: "ok from mid",
126+
from: 4,
127+
missingNo: 4,
128+
}, {
129+
name: "ok from 1 after adding",
130+
from: 1,
131+
do: func() {
132+
if err := walletdb.Update(ctx, db, func(dbtx walletdb.ReadWriteTx) error {
133+
ns := dbtx.ReadWriteBucket(wtxmgrBucketKey)
134+
b3Hash := b3H.BlockHash()
135+
err := putRawCFilter(ns, b3Hash[:], nil)
136+
if err != nil {
137+
return err
138+
}
139+
return nil
140+
}); err != nil {
141+
t.Fatal(err)
142+
}
143+
},
144+
missingNo: 4,
145+
}, {
146+
name: "error once all filters full",
147+
do: func() {
148+
if err := walletdb.Update(ctx, db, func(dbtx walletdb.ReadWriteTx) error {
149+
ns := dbtx.ReadWriteBucket(wtxmgrBucketKey)
150+
b4Hash := b4H.BlockHash()
151+
err := putRawCFilter(ns, b4Hash[:], nil)
152+
if err != nil {
153+
return err
154+
}
155+
return nil
156+
}); err != nil {
157+
t.Fatal(err)
158+
}
159+
},
160+
wantErr: true,
161+
}}
162+
163+
for _, test := range tests {
164+
t.Run(test.name, func(t *testing.T) {
165+
var missingNo int32
166+
if test.do != nil {
167+
test.do()
168+
}
169+
err = walletdb.Update(ctx, db, func(dbtx walletdb.ReadWriteTx) error {
170+
var err error
171+
missingNo, err = MissingCFiltersHeight(dbtx, test.from)
172+
return err
173+
})
174+
if test.wantErr {
175+
if err == nil {
176+
t.Fatal("wanted error but got none")
177+
}
178+
return
179+
}
180+
if err != nil {
181+
t.Fatal(err)
182+
}
183+
if missingNo != test.missingNo {
184+
t.Fatalf("wanted missing number %v but got %v", test.missingNo, missingNo)
185+
}
186+
})
187+
}
188+
}

wallet/wallet.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,9 +1300,14 @@ func (w *Wallet) fetchMissingCFilters(ctx context.Context, n NetworkBackend, pro
13001300

13011301
err := walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error {
13021302
var err error
1303+
var fromHeight int32
1304+
birthday := udb.BirthState(dbtx)
1305+
if birthday != nil {
1306+
fromHeight = int32(birthday.Height)
1307+
}
13031308
missing = w.txStore.IsMissingMainChainCFilters(dbtx)
13041309
if missing {
1305-
height, err = w.txStore.MissingCFiltersHeight(dbtx)
1310+
height, err = udb.MissingCFiltersHeight(dbtx, fromHeight)
13061311
}
13071312
return err
13081313
})
@@ -1339,8 +1344,16 @@ func (w *Wallet) fetchMissingCFilters(ctx context.Context, n NetworkBackend, pro
13391344
}
13401345
_, _, err = w.txStore.CFilterV2(dbtx, &hash)
13411346
if err == nil {
1342-
height += span
1343-
cont = true
1347+
// If there is a gap for some reason, continue from the end of the gap.
1348+
height += 1
1349+
missingHeight, err := udb.MissingCFiltersHeight(dbtx, height)
1350+
if err != nil {
1351+
return err
1352+
}
1353+
if height != missingHeight {
1354+
height = missingHeight
1355+
cont = true
1356+
}
13441357
return nil
13451358
}
13461359
storage = storage[:cap(storage)]

0 commit comments

Comments
 (0)