Skip to content

Commit

Permalink
wallet: Add birthday.
Browse files Browse the repository at this point in the history
  • Loading branch information
JoeGruffins committed Apr 20, 2024
1 parent ad140d2 commit 592825b
Show file tree
Hide file tree
Showing 5 changed files with 292 additions and 7 deletions.
32 changes: 32 additions & 0 deletions spv/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ type Syncer struct {
// Mempool for non-wallet-relevant transactions.
mempool sync.Map // k=chainhash.Hash v=*wire.MsgTx
mempoolAdds chan *chainhash.Hash

birthday *time.Time
birthdaySet atomic.Bool
}

// Notifications struct to contain all of the upcoming callbacks that will
Expand Down Expand Up @@ -134,6 +137,10 @@ func (s *Syncer) SetPersistentPeers(peers []string) {
s.persistentPeers = peers
}

func (s *Syncer) SetBirthday(birthday *time.Time) {
s.birthday = birthday
}

// SetNotifications sets the possible various callbacks that are used
// to notify interested parties to the syncing progress.
func (s *Syncer) SetNotifications(ntfns *Notifications) {
Expand Down Expand Up @@ -385,6 +392,12 @@ func (s *Syncer) Run(ctx context.Context) error {
}
s.fetchHeadersFinished()

if s.birthday != nil && !s.birthdaySet.Swap(true) {
if _, err := s.wallet.SetBirthday(ctx, s.birthday); err != nil {
return err
}
}

// Finally: Perform the initial rescan over the received blocks.
err = s.initialSyncRescan(ctx)
if err != nil {
Expand Down Expand Up @@ -1570,6 +1583,25 @@ func (s *Syncer) initialSyncRescan(ctx context.Context) error {
return nil
}

// Check if we have a birthday set and rescan from there if we do and
// it is after the rescan point.
birthdayHash, birthdayTime, err := s.wallet.Birthday(ctx)
if err != nil {
return err
}

if birthdayHash != nil {
rp, err := s.wallet.BlockHeader(ctx, rescanPoint)
if err != nil {
return err
}
if birthdayTime.After(rp.Timestamp) {
rescanPoint = birthdayHash
log.Infof("Rescanning from birthday block (birthdayHash=%s, birthdayTime=%v)",
birthdayHash, birthdayTime)
}
}

// Perform address/account discovery.
gapLimit := s.wallet.GapLimit()
log.Debugf("Starting address discovery (discoverAccounts=%v, gapLimit=%d, rescanPoint=%v)",
Expand Down
30 changes: 30 additions & 0 deletions wallet/rescan.go
Original file line number Diff line number Diff line change
Expand Up @@ -493,3 +493,33 @@ func (w *Wallet) rescanPoint(dbtx walletdb.ReadTx) (*chainhash.Hash, error) {
}
return &rescanPoint, nil
}

// Birthday
func (w *Wallet) Birthday(ctx context.Context) (*chainhash.Hash, *time.Time, error) {
const op errors.Op = "wallet.Birthday"
var h *chainhash.Hash
var t *time.Time
err := walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error {
h, t = w.txStore.Birthday(dbtx)
return nil
})
if err != nil {
return nil, nil, errors.E(op, err)
}
return h, t, nil
}

// SetBirthdayFromTime
func (w *Wallet) SetBirthday(ctx context.Context, birthday *time.Time) (*chainhash.Hash, error) {
const op errors.Op = "wallet.SetBirthday"
var h *chainhash.Hash
err := walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error {
var err error
h, err = w.txStore.SetBirthday(dbtx, birthday)
return err
})
if err != nil {
return nil, errors.E(op, err)
}
return h, nil
}
15 changes: 8 additions & 7 deletions wallet/udb/txdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,14 @@ var (

// Root (namespace) bucket keys
var (
rootCreateDate = []byte("date")
rootVersion = []byte("vers")
rootMinedBalance = []byte("bal")
rootTipBlock = []byte("tip")
rootHaveCFilters = []byte("havecfilters")
rootLastTxsBlock = []byte("lasttxsblock")
rootVSPHostIndex = []byte("vsphostindex")
rootCreateDate = []byte("date")
rootVersion = []byte("vers")
rootMinedBalance = []byte("bal")
rootTipBlock = []byte("tip")
rootHaveCFilters = []byte("havecfilters")
rootLastTxsBlock = []byte("lasttxsblock")
rootVSPHostIndex = []byte("vsphostindex")
rootBirthdayHashAndTime = []byte("birthdayhashandtime")
)

// The root bucket's mined balance k/v pair records the total balance for all
Expand Down
93 changes: 93 additions & 0 deletions wallet/udb/txmined.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,99 @@ func (s *Store) UpdateProcessedTxsBlockMarker(dbtx walletdb.ReadWriteTx, hash *c
return nil
}

// Birthday returns the wallet's birthday time and a blockhash before that time
// if set. If not set nil values are returned.
func (s *Store) Birthday(dbtx walletdb.ReadTx) (*chainhash.Hash, *time.Time) {
const timeBits = 8
ns := dbtx.ReadBucket(wtxmgrBucketKey)
v := ns.Get(rootBirthdayHashAndTime)
if len(v) != chainhash.HashSize+timeBits {
return nil, nil
}
var h chainhash.Hash
copy(h[:], v[:chainhash.HashSize])
t := time.Unix(int64(byteOrder.Uint64(v[chainhash.HashSize:])), 0)
return &h, &t
}

// SetBirthday sets a birthday hash and time from a birthday time. The hash will
// be from the block before the birthday time.
//
// [0:32] Block header hash (32 bytes)
// [32:40] Birthday time (8 bytes)
func (s *Store) SetBirthday(dbtx walletdb.ReadWriteTx, birthday *time.Time) (*chainhash.Hash, error) {
ns := dbtx.ReadWriteBucket(wtxmgrBucketKey)
_, bHeight := s.MainChainTip(dbtx)
tipHeight := bHeight
var err error
genesisTime, err := fetchBlockTime(ns, 0)
if err != nil {
return nil, fmt.Errorf("unable to get genesis blocktime: %w", err)
}
genesisUnix := genesisTime.Unix()
birthdayUnix := birthday.Unix()
lastHeight := bHeight
timeBeforeBirthday := birthdayUnix - genesisUnix
goBack := func() error {
for {
if bHeight == tipHeight {
return nil
}
bHeight += 1
bTime, err := fetchBlockTime(ns, bHeight)
if err != nil {
return err
}
btUnix := bTime.Unix()
// Go until one header past the birthday time and use
// the one before that.
if btUnix >= birthdayUnix {
bHeight -= 1
return nil
}
}
}
// Look backwards through the chain for a header time before the
// birthday. Use a fraction of the current chain liftime compared to the
// time before the birthday to estimate where the block should be.
for {
if bHeight == 0 {
break
}
bTime, err := fetchBlockTime(ns, bHeight)
if err != nil {
return nil, fmt.Errorf("unable to fetch block at height %d: %w", bHeight, err)
}
bTimeUnix := bTime.Unix()
if bTimeUnix < birthdayUnix {
if err := goBack(); err != nil {
return nil, err
}
break
}
chainLifeTime := bTimeUnix - genesisUnix
bHeight = int32(int64(bHeight) * timeBeforeBirthday / chainLifeTime)
if bHeight == lastHeight {
bHeight -= 1
}
lastHeight = bHeight
}
br, err := fetchBlockRecord(ns, bHeight)
if err != nil {
return nil, err
}
const timeBits = 8
h := br.Block.Hash
v := make([]byte, chainhash.HashSize+timeBits)
copy(v[:], h[:])
// Saving the passed birthday rather than the blocktime.
byteOrder.PutUint64(v[chainhash.HashSize:], uint64(birthday.Unix()))
if err := ns.Put(rootBirthdayHashAndTime, v); err != nil {
return nil, err
}
return &h, nil
}

// IsMissingMainChainCFilters returns whether all compact filters for main chain
// blocks have been recorded to the database after the upgrade which began to
// require them to extend the main chain. If compact filters are missing, they
Expand Down
129 changes: 129 additions & 0 deletions wallet/udb/txmined_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright (c) 2024 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package udb

import (
"bytes"
"context"
"math/rand"
"testing"
"time"

"decred.org/dcrwallet/v4/wallet/walletdb"
"github.com/decred/dcrd/chaincfg/chainhash"
"github.com/decred/dcrd/wire"
)

var r = rand.New(rand.NewSource(time.Now().Unix()))

func TestSetBirthday(t *testing.T) {
var numTxn = 5000
if testing.Short() {
numTxn = 500
}
ctx := context.Background()
db, _, txStore, _, teardown, err := cloneDB(ctx, "mgr_watching_only.kv")
defer teardown()
if err != nil {
t.Fatal(err)
}
now := time.Now()
middleTime := now.Add(time.Duration(numTxn) * time.Minute * 5)
middleTimeHeaderHeight := 0

headers := make([]*wire.BlockHeader, numTxn)
hashToHeight := make(map[chainhash.Hash]int)
headerTime := now
for i := range headers {
h := wire.BlockHeader{
Height: uint32(i),
Timestamp: headerTime,
}
headers[i] = &h
if headerTime.Before(middleTime) {
middleTimeHeaderHeight = i
}
hashToHeight[h.BlockHash()] = i
headerTime = headerTime.Add(time.Duration(r.Intn(20))*time.Minute + time.Minute*5)
}

err = walletdb.Update(ctx, db, func(dbtx walletdb.ReadWriteTx) error {
ns := dbtx.ReadWriteBucket(wtxmgrBucketKey)

for i, header := range headers {
var rbh RawBlockHeader
buf := bytes.NewBuffer(rbh[:0])
err = header.Serialize(buf)
if err != nil {
return err
}
h := header.BlockHash()
val := valueBlockRecordEmptyFromHeader(h[:], rbh[:])
err = putRawBlockRecord(ns, keyBlockRecord(int32(i)), val[:])
if err != nil {
return err
}
}
tipHeader := headers[len(headers)-1]
tipHash := tipHeader.BlockHash()
var rbh RawBlockHeader
buf := bytes.NewBuffer(rbh[:0])
err = tipHeader.Serialize(buf)
if err != nil {
return err
}
if err := putRawBlockHeader(ns, tipHash[:], rbh[:]); err != nil {
return err
}
return ns.Put(rootTipBlock, tipHash[:])
})
if err != nil {
t.Fatal(err)
}

tests := []struct {
name string
birthday time.Time
wantHeight int
}{{
name: "ok somewhere in the middle",
birthday: middleTime,
wantHeight: middleTimeHeaderHeight,
}, {
name: "ok after best block time",
birthday: headerTime.Add(time.Hour * 48),
wantHeight: numTxn - 1,
}, {
name: "ok before genesis",
birthday: now.Add(time.Minute * -5),
wantHeight: 0,
}}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err = walletdb.Update(ctx, db, func(dbtx walletdb.ReadWriteTx) error {
_, err := txStore.SetBirthday(dbtx, &test.birthday)
return err
})
if err != nil {
t.Fatal(err)
}
var h *chainhash.Hash
err = walletdb.View(ctx, db, func(dbtx walletdb.ReadTx) error {
h, _ = txStore.Birthday(dbtx)
return nil
})
if err != nil {
t.Fatal(err)
}
height := hashToHeight[*h]
if height != test.wantHeight {
t.Fatalf("birthday at height %d not at expected height %d",
height, test.wantHeight)
}
})
}

}

0 comments on commit 592825b

Please sign in to comment.