Skip to content

Commit 6a5381a

Browse files
committed
Merge bitcoin/bitcoin#20591: wallet, bugfix: fix ComputeTimeSmart function during rescanning process.
240ea29 doc: update doxygen documention of ComputeTimeSmart() and AddToWalletIfInvolvingMe() regarding rescanning_old_block parameter (BitcoinTsunami) d6eb39a test: add functional test to check transaction time determination during block rescanning (BitcoinTsunami) 07b44f1 wallet: fix ComputeTimeSmart algorithm to use blocktime during old block rescanning (BitcoinTsunami) Pull request description: The function ComputeTimeSmart in wallet.cpp assume that transaction are discovered in the right order. Moreover the 'smarttime' determination algorithm is coded with realtime scenario in mind and not rescanning of old block. The functional test demonstrate that if the user import a wallet, then rescan only recent history, and then rescan the entire history, the older transaction discovered would have an incorrect time determination. In the context of rescanning old block, the only time value that as a meaning is the blocktime. That's why I've fixed the problem with a simple separation between rescanning of old block and realtime time determination. The fix is written to have no impact on every realtime scenario and only impact the behaviour during a rescanning process. This PR Fixes #20181. To be fair, I don't think that this bug could be triggered with the wallet GUI, because it always proceed with a proper rescan. But RPC API provide the possibility to trigger it. I've discovered it, because Specter desktop v0.10.0 was impacted. (cryptoadvance/specter-desktop#680). ACKs for top commit: jonatack: ACK 240ea29 per `git diff b92d552 240ea29`, re-verified rebase to latest master + debug build clean + new test passes on the branch and fails on master, only change since my review a few hours ago is incorporation of latest review suggestions meshcollider: re-utACK 240ea29 Tree-SHA512: 514b02e41d011ddfa325f5e8080b93800e1ea4ed5853fa420670a6ac700e8b463000dbea65f8ced8565cfb950c7f51b69154034dcb111e67aca3b964a0061494
2 parents b55232a + 240ea29 commit 6a5381a

File tree

4 files changed

+208
-36
lines changed

4 files changed

+208
-36
lines changed

src/wallet/wallet.cpp

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -884,7 +884,7 @@ bool CWallet::IsSpentKey(const uint256& hash, unsigned int n) const
884884
return false;
885885
}
886886

887-
CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const CWalletTx::Confirmation& confirm, const UpdateWalletTxFn& update_wtx, bool fFlushOnClose)
887+
CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const CWalletTx::Confirmation& confirm, const UpdateWalletTxFn& update_wtx, bool fFlushOnClose, bool rescanning_old_block)
888888
{
889889
LOCK(cs_wallet);
890890

@@ -914,7 +914,7 @@ CWalletTx* CWallet::AddToWallet(CTransactionRef tx, const CWalletTx::Confirmatio
914914
wtx.nTimeReceived = chain().getAdjustedTime();
915915
wtx.nOrderPos = IncOrderPosNext(&batch);
916916
wtx.m_it_wtxOrdered = wtxOrdered.insert(std::make_pair(wtx.nOrderPos, &wtx));
917-
wtx.nTimeSmart = ComputeTimeSmart(wtx);
917+
wtx.nTimeSmart = ComputeTimeSmart(wtx, rescanning_old_block);
918918
AddToSpends(hash, &batch);
919919
}
920920

@@ -1031,7 +1031,7 @@ bool CWallet::LoadToWallet(const uint256& hash, const UpdateWalletTxFn& fill_wtx
10311031
return true;
10321032
}
10331033

1034-
bool CWallet::AddToWalletIfInvolvingMe(const CTransactionRef& ptx, CWalletTx::Confirmation confirm, bool fUpdate)
1034+
bool CWallet::AddToWalletIfInvolvingMe(const CTransactionRef& ptx, CWalletTx::Confirmation confirm, bool fUpdate, bool rescanning_old_block)
10351035
{
10361036
const CTransaction& tx = *ptx;
10371037
{
@@ -1069,7 +1069,7 @@ bool CWallet::AddToWalletIfInvolvingMe(const CTransactionRef& ptx, CWalletTx::Co
10691069

10701070
// Block disconnection override an abandoned tx as unconfirmed
10711071
// which means user may have to call abandontransaction again
1072-
return AddToWallet(MakeTransactionRef(tx), confirm, /* update_wtx= */ nullptr, /* fFlushOnClose= */ false);
1072+
return AddToWallet(MakeTransactionRef(tx), confirm, /* update_wtx= */ nullptr, /* fFlushOnClose= */ false, rescanning_old_block);
10731073
}
10741074
}
10751075
return false;
@@ -1198,9 +1198,9 @@ void CWallet::MarkConflicted(const uint256& hashBlock, int conflicting_height, c
11981198
}
11991199
}
12001200

1201-
void CWallet::SyncTransaction(const CTransactionRef& ptx, CWalletTx::Confirmation confirm, bool update_tx)
1201+
void CWallet::SyncTransaction(const CTransactionRef& ptx, CWalletTx::Confirmation confirm, bool update_tx, bool rescanning_old_block)
12021202
{
1203-
if (!AddToWalletIfInvolvingMe(ptx, confirm, update_tx))
1203+
if (!AddToWalletIfInvolvingMe(ptx, confirm, update_tx, rescanning_old_block))
12041204
return; // Not one of ours
12051205

12061206
// If a transaction changes 'conflicted' state, that changes the balance
@@ -1643,7 +1643,7 @@ CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_bloc
16431643
break;
16441644
}
16451645
for (size_t posInBlock = 0; posInBlock < block.vtx.size(); ++posInBlock) {
1646-
SyncTransaction(block.vtx[posInBlock], {CWalletTx::Status::CONFIRMED, block_height, block_hash, (int)posInBlock}, fUpdate);
1646+
SyncTransaction(block.vtx[posInBlock], {CWalletTx::Status::CONFIRMED, block_height, block_hash, (int)posInBlock}, fUpdate, /* rescanning_old_block */ true);
16471647
}
16481648
// scan succeeded, record block as most recent successfully scanned
16491649
result.last_scanned_block = block_hash;
@@ -2384,6 +2384,8 @@ void CWallet::GetKeyBirthTimes(std::map<CKeyID, int64_t>& mapKeyBirth) const {
23842384
* - If sending a transaction, assign its timestamp to the current time.
23852385
* - If receiving a transaction outside a block, assign its timestamp to the
23862386
* current time.
2387+
* - If receiving a transaction during a rescanning process, assign all its
2388+
* (not already known) transactions' timestamps to the block time.
23872389
* - If receiving a block with a future timestamp, assign all its (not already
23882390
* known) transactions' timestamps to the current time.
23892391
* - If receiving a block with a past timestamp, before the most recent known
@@ -2398,38 +2400,43 @@ void CWallet::GetKeyBirthTimes(std::map<CKeyID, int64_t>& mapKeyBirth) const {
23982400
* https://bitcointalk.org/?topic=54527, or
23992401
* https://github.com/bitcoin/bitcoin/pull/1393.
24002402
*/
2401-
unsigned int CWallet::ComputeTimeSmart(const CWalletTx& wtx) const
2403+
unsigned int CWallet::ComputeTimeSmart(const CWalletTx& wtx, bool rescanning_old_block) const
24022404
{
24032405
unsigned int nTimeSmart = wtx.nTimeReceived;
24042406
if (!wtx.isUnconfirmed() && !wtx.isAbandoned()) {
24052407
int64_t blocktime;
2406-
if (chain().findBlock(wtx.m_confirm.hashBlock, FoundBlock().time(blocktime))) {
2407-
int64_t latestNow = wtx.nTimeReceived;
2408-
int64_t latestEntry = 0;
2409-
2410-
// Tolerate times up to the last timestamp in the wallet not more than 5 minutes into the future
2411-
int64_t latestTolerated = latestNow + 300;
2412-
const TxItems& txOrdered = wtxOrdered;
2413-
for (auto it = txOrdered.rbegin(); it != txOrdered.rend(); ++it) {
2414-
CWalletTx* const pwtx = it->second;
2415-
if (pwtx == &wtx) {
2416-
continue;
2417-
}
2418-
int64_t nSmartTime;
2419-
nSmartTime = pwtx->nTimeSmart;
2420-
if (!nSmartTime) {
2421-
nSmartTime = pwtx->nTimeReceived;
2422-
}
2423-
if (nSmartTime <= latestTolerated) {
2424-
latestEntry = nSmartTime;
2425-
if (nSmartTime > latestNow) {
2426-
latestNow = nSmartTime;
2408+
int64_t block_max_time;
2409+
if (chain().findBlock(wtx.m_confirm.hashBlock, FoundBlock().time(blocktime).maxTime(block_max_time))) {
2410+
if (rescanning_old_block) {
2411+
nTimeSmart = block_max_time;
2412+
} else {
2413+
int64_t latestNow = wtx.nTimeReceived;
2414+
int64_t latestEntry = 0;
2415+
2416+
// Tolerate times up to the last timestamp in the wallet not more than 5 minutes into the future
2417+
int64_t latestTolerated = latestNow + 300;
2418+
const TxItems& txOrdered = wtxOrdered;
2419+
for (auto it = txOrdered.rbegin(); it != txOrdered.rend(); ++it) {
2420+
CWalletTx* const pwtx = it->second;
2421+
if (pwtx == &wtx) {
2422+
continue;
2423+
}
2424+
int64_t nSmartTime;
2425+
nSmartTime = pwtx->nTimeSmart;
2426+
if (!nSmartTime) {
2427+
nSmartTime = pwtx->nTimeReceived;
2428+
}
2429+
if (nSmartTime <= latestTolerated) {
2430+
latestEntry = nSmartTime;
2431+
if (nSmartTime > latestNow) {
2432+
latestNow = nSmartTime;
2433+
}
2434+
break;
24272435
}
2428-
break;
24292436
}
2430-
}
24312437

2432-
nTimeSmart = std::max(latestEntry, std::min(blocktime, latestNow));
2438+
nTimeSmart = std::max(latestEntry, std::min(blocktime, latestNow));
2439+
}
24332440
} else {
24342441
WalletLogPrintf("%s: found %s in block %s not in index\n", __func__, wtx.GetHash().ToString(), wtx.m_confirm.hashBlock.ToString());
24352442
}

src/wallet/wallet.h

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,11 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
271271
* abandoned is an indication that it is not safe to be considered abandoned.
272272
* Abandoned state should probably be more carefully tracked via different
273273
* posInBlock signals or by checking mempool presence when necessary.
274+
*
275+
* Should be called with rescanning_old_block set to true, if the transaction is
276+
* not discovered in real time, but during a rescan of old blocks.
274277
*/
275-
bool AddToWalletIfInvolvingMe(const CTransactionRef& tx, CWalletTx::Confirmation confirm, bool fUpdate) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
278+
bool AddToWalletIfInvolvingMe(const CTransactionRef& tx, CWalletTx::Confirmation confirm, bool fUpdate, bool rescanning_old_block) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
276279

277280
/** Mark a transaction (and its in-wallet descendants) as conflicting with a particular block. */
278281
void MarkConflicted(const uint256& hashBlock, int conflicting_height, const uint256& hashTx);
@@ -284,7 +287,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
284287

285288
/* Used by TransactionAddedToMemorypool/BlockConnected/Disconnected/ScanForWalletTransactions.
286289
* Should be called with non-zero block_hash and posInBlock if this is for a transaction that is included in a block. */
287-
void SyncTransaction(const CTransactionRef& tx, CWalletTx::Confirmation confirm, bool update_tx = true) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
290+
void SyncTransaction(const CTransactionRef& tx, CWalletTx::Confirmation confirm, bool update_tx = true, bool rescanning_old_block = false) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
288291

289292
/** WalletFlags set on this wallet. */
290293
std::atomic<uint64_t> m_wallet_flags{0};
@@ -484,7 +487,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
484487
bool EncryptWallet(const SecureString& strWalletPassphrase);
485488

486489
void GetKeyBirthTimes(std::map<CKeyID, int64_t> &mapKeyBirth) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
487-
unsigned int ComputeTimeSmart(const CWalletTx& wtx) const;
490+
unsigned int ComputeTimeSmart(const CWalletTx& wtx, bool rescanning_old_block) const;
488491

489492
/**
490493
* Increment the next transaction order id
@@ -503,7 +506,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
503506
//! @return true if wtx is changed and needs to be saved to disk, otherwise false
504507
using UpdateWalletTxFn = std::function<bool(CWalletTx& wtx, bool new_tx)>;
505508

506-
CWalletTx* AddToWallet(CTransactionRef tx, const CWalletTx::Confirmation& confirm, const UpdateWalletTxFn& update_wtx=nullptr, bool fFlushOnClose=true);
509+
CWalletTx* AddToWallet(CTransactionRef tx, const CWalletTx::Confirmation& confirm, const UpdateWalletTxFn& update_wtx=nullptr, bool fFlushOnClose=true, bool rescanning_old_block = false);
507510
bool LoadToWallet(const uint256& hash, const UpdateWalletTxFn& fill_wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
508511
void transactionAddedToMempool(const CTransactionRef& tx, uint64_t mempool_sequence) override;
509512
void blockConnected(const CBlock& block, int height) override;

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@
175175
'rpc_rawtransaction.py --legacy-wallet',
176176
'rpc_rawtransaction.py --descriptors',
177177
'wallet_groups.py --legacy-wallet',
178+
'wallet_transactiontime_rescan.py',
178179
'p2p_addrv2_relay.py',
179180
'wallet_groups.py --descriptors',
180181
'p2p_compactblocks_hb.py',
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2018-2019 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""Test transaction time during old block rescanning
6+
"""
7+
8+
import time
9+
10+
from test_framework.blocktools import COINBASE_MATURITY
11+
from test_framework.test_framework import BitcoinTestFramework
12+
from test_framework.util import (
13+
assert_equal
14+
)
15+
16+
17+
class TransactionTimeRescanTest(BitcoinTestFramework):
18+
def set_test_params(self):
19+
self.setup_clean_chain = False
20+
self.num_nodes = 3
21+
22+
def skip_test_if_missing_module(self):
23+
self.skip_if_no_wallet()
24+
25+
def run_test(self):
26+
self.log.info('Prepare nodes and wallet')
27+
28+
minernode = self.nodes[0] # node used to mine BTC and create transactions
29+
usernode = self.nodes[1] # user node with correct time
30+
restorenode = self.nodes[2] # node used to restore user wallet and check time determination in ComputeSmartTime (wallet.cpp)
31+
32+
# time constant
33+
cur_time = int(time.time())
34+
ten_days = 10 * 24 * 60 * 60
35+
36+
# synchronize nodes and time
37+
self.sync_all()
38+
minernode.setmocktime(cur_time)
39+
usernode.setmocktime(cur_time)
40+
restorenode.setmocktime(cur_time)
41+
42+
# prepare miner wallet
43+
minernode.createwallet(wallet_name='default')
44+
miner_wallet = minernode.get_wallet_rpc('default')
45+
m1 = miner_wallet.getnewaddress()
46+
47+
# prepare the user wallet with 3 watch only addresses
48+
wo1 = usernode.getnewaddress()
49+
wo2 = usernode.getnewaddress()
50+
wo3 = usernode.getnewaddress()
51+
52+
usernode.createwallet(wallet_name='wo', disable_private_keys=True)
53+
wo_wallet = usernode.get_wallet_rpc('wo')
54+
55+
wo_wallet.importaddress(wo1)
56+
wo_wallet.importaddress(wo2)
57+
wo_wallet.importaddress(wo3)
58+
59+
self.log.info('Start transactions')
60+
61+
# check blockcount
62+
assert_equal(minernode.getblockcount(), 200)
63+
64+
# generate some btc to create transactions and check blockcount
65+
initial_mine = COINBASE_MATURITY + 1
66+
minernode.generatetoaddress(initial_mine, m1)
67+
assert_equal(minernode.getblockcount(), initial_mine + 200)
68+
69+
# synchronize nodes and time
70+
self.sync_all()
71+
minernode.setmocktime(cur_time + ten_days)
72+
usernode.setmocktime(cur_time + ten_days)
73+
restorenode.setmocktime(cur_time + ten_days)
74+
# send 10 btc to user's first watch-only address
75+
self.log.info('Send 10 btc to user')
76+
miner_wallet.sendtoaddress(wo1, 10)
77+
78+
# generate blocks and check blockcount
79+
minernode.generatetoaddress(COINBASE_MATURITY, m1)
80+
assert_equal(minernode.getblockcount(), initial_mine + 300)
81+
82+
# synchronize nodes and time
83+
self.sync_all()
84+
minernode.setmocktime(cur_time + ten_days + ten_days)
85+
usernode.setmocktime(cur_time + ten_days + ten_days)
86+
restorenode.setmocktime(cur_time + ten_days + ten_days)
87+
# send 5 btc to our second watch-only address
88+
self.log.info('Send 5 btc to user')
89+
miner_wallet.sendtoaddress(wo2, 5)
90+
91+
# generate blocks and check blockcount
92+
minernode.generatetoaddress(COINBASE_MATURITY, m1)
93+
assert_equal(minernode.getblockcount(), initial_mine + 400)
94+
95+
# synchronize nodes and time
96+
self.sync_all()
97+
minernode.setmocktime(cur_time + ten_days + ten_days + ten_days)
98+
usernode.setmocktime(cur_time + ten_days + ten_days + ten_days)
99+
restorenode.setmocktime(cur_time + ten_days + ten_days + ten_days)
100+
# send 1 btc to our third watch-only address
101+
self.log.info('Send 1 btc to user')
102+
miner_wallet.sendtoaddress(wo3, 1)
103+
104+
# generate more blocks and check blockcount
105+
minernode.generatetoaddress(COINBASE_MATURITY, m1)
106+
assert_equal(minernode.getblockcount(), initial_mine + 500)
107+
108+
self.log.info('Check user\'s final balance and transaction count')
109+
assert_equal(wo_wallet.getbalance(), 16)
110+
assert_equal(len(wo_wallet.listtransactions()), 3)
111+
112+
self.log.info('Check transaction times')
113+
for tx in wo_wallet.listtransactions():
114+
if tx['address'] == wo1:
115+
assert_equal(tx['blocktime'], cur_time + ten_days)
116+
assert_equal(tx['time'], cur_time + ten_days)
117+
elif tx['address'] == wo2:
118+
assert_equal(tx['blocktime'], cur_time + ten_days + ten_days)
119+
assert_equal(tx['time'], cur_time + ten_days + ten_days)
120+
elif tx['address'] == wo3:
121+
assert_equal(tx['blocktime'], cur_time + ten_days + ten_days + ten_days)
122+
assert_equal(tx['time'], cur_time + ten_days + ten_days + ten_days)
123+
124+
# restore user wallet without rescan
125+
self.log.info('Restore user wallet on another node without rescan')
126+
restorenode.createwallet(wallet_name='wo', disable_private_keys=True)
127+
restorewo_wallet = restorenode.get_wallet_rpc('wo')
128+
129+
restorewo_wallet.importaddress(wo1, rescan=False)
130+
restorewo_wallet.importaddress(wo2, rescan=False)
131+
restorewo_wallet.importaddress(wo3, rescan=False)
132+
133+
# check user has 0 balance and no transactions
134+
assert_equal(restorewo_wallet.getbalance(), 0)
135+
assert_equal(len(restorewo_wallet.listtransactions()), 0)
136+
137+
# proceed to rescan, first with an incomplete one, then with a full rescan
138+
self.log.info('Rescan last history part')
139+
restorewo_wallet.rescanblockchain(initial_mine + 350)
140+
self.log.info('Rescan all history')
141+
restorewo_wallet.rescanblockchain()
142+
143+
self.log.info('Check user\'s final balance and transaction count after restoration')
144+
assert_equal(restorewo_wallet.getbalance(), 16)
145+
assert_equal(len(restorewo_wallet.listtransactions()), 3)
146+
147+
self.log.info('Check transaction times after restoration')
148+
for tx in restorewo_wallet.listtransactions():
149+
if tx['address'] == wo1:
150+
assert_equal(tx['blocktime'], cur_time + ten_days)
151+
assert_equal(tx['time'], cur_time + ten_days)
152+
elif tx['address'] == wo2:
153+
assert_equal(tx['blocktime'], cur_time + ten_days + ten_days)
154+
assert_equal(tx['time'], cur_time + ten_days + ten_days)
155+
elif tx['address'] == wo3:
156+
assert_equal(tx['blocktime'], cur_time + ten_days + ten_days + ten_days)
157+
assert_equal(tx['time'], cur_time + ten_days + ten_days + ten_days)
158+
159+
160+
if __name__ == '__main__':
161+
TransactionTimeRescanTest().main()

0 commit comments

Comments
 (0)