Skip to content

Commit 5e20238

Browse files
committed
Merge #16624: wallet: encapsulate transactions state
442a87c Add a test wallet_reorgsrestore (Antoine Riard) 40ede99 Modify wallet tx status if has been reorged out (Antoine Riard) 7e89994 Remove SyncTransaction for conflicted txn in CWallet::BlockConnected (Antoine Riard) a31be09 Encapsulate tx status in a Confirmation struct (Antoine Riard) Pull request description: While working on #15931, I've tried to rationalize tx state management to ease integration of block height tracking per-wallet tx. We currently rely on a combination of `hashBlock` and `nIndex` with magic value to determine tx confirmation, conflicted or abandoned state. It's hard to reason and error-prone. To solve that, we encapsulate these fields in a `TxConfirmation` struct and introduce a `TxState` member that we update accordingly at block connection/disconnection. Following jnewbery [recommendation](bitcoin/bitcoin#15931 (comment)), I've taken these changes in its own commit, and open a PR to get them first. It would ease review of aforementioned PR, but above all should ease fixing of long-term issues like : * bitcoin/bitcoin#7315 (but maybe we should abandon abandontransaction or relieve it to only free outpoints not track the transaction as abandoned in itself, need its own discussion) * bitcoin/bitcoin#8692 where we should cancel conflicted state of transactions chain smoothly * `MarkConflicted` in `LoadToWallet` is likely useless if we track conflicts rights at block connection Main changes of this PR to get right are tx update in `AddToWallet` and serialization/deserialization logic. ACKs for top commit: meshcollider: Light re-Code Review ACK 442a87c ryanofsky: utACK 442a87c. Changes since last review are switching from `hasChain` to `LockChain` and removing chain lock in `WalletBatch::LoadWallet` that's redundant with the new lock still added in `CWallet::LoadWallet`, and fixing python test race condition. Tree-SHA512: 029209e006de0240436817204e69e548c5665e2b0721b214510e7aba7eba130a5eab441d3a1ad95bd6426114dd27390492c77bf4560a9610009b32cd0a1f72f7
2 parents 5667b0d + 442a87c commit 5e20238

File tree

8 files changed

+261
-87
lines changed

8 files changed

+261
-87
lines changed

src/interfaces/wallet.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ WalletTx MakeWalletTx(interfaces::Chain::Lock& locked_chain, CWallet& wallet, co
6565
WalletTxStatus MakeWalletTxStatus(interfaces::Chain::Lock& locked_chain, const CWalletTx& wtx)
6666
{
6767
WalletTxStatus result;
68-
result.block_height = locked_chain.getBlockHeight(wtx.hashBlock).get_value_or(std::numeric_limits<int>::max());
68+
result.block_height = locked_chain.getBlockHeight(wtx.m_confirm.hashBlock).get_value_or(std::numeric_limits<int>::max());
6969
result.blocks_to_maturity = wtx.GetBlocksToMaturity(locked_chain);
7070
result.depth_in_main_chain = wtx.GetDepthInMainChain(locked_chain);
7171
result.time_received = wtx.nTimeReceived;

src/wallet/rpcdump.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,8 +384,7 @@ UniValue importprunedfunds(const JSONRPCRequest& request)
384384
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Something wrong with merkleblock");
385385
}
386386

387-
wtx.nIndex = txnIndex;
388-
wtx.hashBlock = merkleBlock.header.GetHash();
387+
wtx.SetConf(CWalletTx::Status::CONFIRMED, merkleBlock.header.GetHash(), txnIndex);
389388

390389
auto locked_chain = pwallet->chain().lock();
391390
LOCK(pwallet->cs_wallet);

src/wallet/rpcwallet.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,10 @@ static void WalletTxToJSON(interfaces::Chain& chain, interfaces::Chain::Lock& lo
134134
entry.pushKV("generated", true);
135135
if (confirms > 0)
136136
{
137-
entry.pushKV("blockhash", wtx.hashBlock.GetHex());
138-
entry.pushKV("blockindex", wtx.nIndex);
137+
entry.pushKV("blockhash", wtx.m_confirm.hashBlock.GetHex());
138+
entry.pushKV("blockindex", wtx.m_confirm.nIndex);
139139
int64_t block_time;
140-
bool found_block = chain.findBlock(wtx.hashBlock, nullptr /* block */, &block_time);
140+
bool found_block = chain.findBlock(wtx.m_confirm.hashBlock, nullptr /* block */, &block_time);
141141
assert(found_block);
142142
entry.pushKV("blocktime", block_time);
143143
} else {

src/wallet/test/wallet_tests.cpp

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,7 @@ BOOST_FIXTURE_TEST_CASE(coin_mark_dirty_immature_credit, TestChain100Setup)
249249
LockAssertion lock(::cs_main);
250250
LOCK(wallet.cs_wallet);
251251

252-
wtx.hashBlock = ::ChainActive().Tip()->GetBlockHash();
253-
wtx.nIndex = 0;
252+
wtx.SetConf(CWalletTx::Status::CONFIRMED, ::ChainActive().Tip()->GetBlockHash(), 0);
254253

255254
// Call GetImmatureCredit() once before adding the key to the wallet to
256255
// cache the current immature credit amount, which is 0.
@@ -281,14 +280,19 @@ static int64_t AddTx(CWallet& wallet, uint32_t lockTime, int64_t mockTime, int64
281280
}
282281

283282
CWalletTx wtx(&wallet, MakeTransactionRef(tx));
284-
if (block) {
285-
wtx.SetMerkleBranch(block->GetBlockHash(), 0);
286-
}
287-
{
288-
LOCK(cs_main);
283+
LOCK(cs_main);
284+
LOCK(wallet.cs_wallet);
285+
// If transaction is already in map, to avoid inconsistencies, unconfirmation
286+
// is needed before confirm again with different block.
287+
std::map<uint256, CWalletTx>::iterator it = wallet.mapWallet.find(wtx.GetHash());
288+
if (it != wallet.mapWallet.end()) {
289+
wtx.setUnconfirmed();
289290
wallet.AddToWallet(wtx);
290291
}
291-
LOCK(wallet.cs_wallet);
292+
if (block) {
293+
wtx.SetConf(CWalletTx::Status::CONFIRMED, block->GetBlockHash(), 0);
294+
}
295+
wallet.AddToWallet(wtx);
292296
return wallet.mapWallet.at(wtx.GetHash()).nTimeSmart;
293297
}
294298

@@ -382,7 +386,7 @@ class ListCoinsTestingSetup : public TestChain100Setup
382386
LOCK(wallet->cs_wallet);
383387
auto it = wallet->mapWallet.find(tx->GetHash());
384388
BOOST_CHECK(it != wallet->mapWallet.end());
385-
it->second.SetMerkleBranch(::ChainActive().Tip()->GetBlockHash(), 1);
389+
it->second.SetConf(CWalletTx::Status::CONFIRMED, ::ChainActive().Tip()->GetBlockHash(), 1);
386390
return it->second;
387391
}
388392

src/wallet/wallet.cpp

Lines changed: 65 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,22 +1110,14 @@ bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose)
11101110
bool fUpdated = false;
11111111
if (!fInsertedNew)
11121112
{
1113-
// Merge
1114-
if (!wtxIn.hashUnset() && wtxIn.hashBlock != wtx.hashBlock)
1115-
{
1116-
wtx.hashBlock = wtxIn.hashBlock;
1117-
fUpdated = true;
1118-
}
1119-
// If no longer abandoned, update
1120-
if (wtxIn.hashBlock.IsNull() && wtx.isAbandoned())
1121-
{
1122-
wtx.hashBlock = wtxIn.hashBlock;
1123-
fUpdated = true;
1124-
}
1125-
if (wtxIn.nIndex != -1 && (wtxIn.nIndex != wtx.nIndex))
1126-
{
1127-
wtx.nIndex = wtxIn.nIndex;
1113+
if (wtxIn.m_confirm.status != wtx.m_confirm.status) {
1114+
wtx.m_confirm.status = wtxIn.m_confirm.status;
1115+
wtx.m_confirm.nIndex = wtxIn.m_confirm.nIndex;
1116+
wtx.m_confirm.hashBlock = wtxIn.m_confirm.hashBlock;
11281117
fUpdated = true;
1118+
} else {
1119+
assert(wtx.m_confirm.nIndex == wtxIn.m_confirm.nIndex);
1120+
assert(wtx.m_confirm.hashBlock == wtxIn.m_confirm.hashBlock);
11291121
}
11301122
if (wtxIn.fFromMe && wtxIn.fFromMe != wtx.fFromMe)
11311123
{
@@ -1172,8 +1164,19 @@ bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose)
11721164
return true;
11731165
}
11741166

1175-
void CWallet::LoadToWallet(const CWalletTx& wtxIn)
1167+
void CWallet::LoadToWallet(CWalletTx& wtxIn)
11761168
{
1169+
// If wallet doesn't have a chain (e.g wallet-tool), lock can't be taken.
1170+
auto locked_chain = LockChain();
1171+
// If tx hasn't been reorged out of chain while wallet being shutdown
1172+
// change tx status to UNCONFIRMED and reset hashBlock/nIndex.
1173+
if (!wtxIn.m_confirm.hashBlock.IsNull()) {
1174+
if (locked_chain && !locked_chain->getBlockHeight(wtxIn.m_confirm.hashBlock)) {
1175+
wtxIn.setUnconfirmed();
1176+
wtxIn.m_confirm.hashBlock = uint256();
1177+
wtxIn.m_confirm.nIndex = 0;
1178+
}
1179+
}
11771180
uint256 hash = wtxIn.GetHash();
11781181
const auto& ins = mapWallet.emplace(hash, wtxIn);
11791182
CWalletTx& wtx = ins.first->second;
@@ -1186,14 +1189,14 @@ void CWallet::LoadToWallet(const CWalletTx& wtxIn)
11861189
auto it = mapWallet.find(txin.prevout.hash);
11871190
if (it != mapWallet.end()) {
11881191
CWalletTx& prevtx = it->second;
1189-
if (prevtx.nIndex == -1 && !prevtx.hashUnset()) {
1190-
MarkConflicted(prevtx.hashBlock, wtx.GetHash());
1192+
if (prevtx.isConflicted()) {
1193+
MarkConflicted(prevtx.m_confirm.hashBlock, wtx.GetHash());
11911194
}
11921195
}
11931196
}
11941197
}
11951198

1196-
bool CWallet::AddToWalletIfInvolvingMe(const CTransactionRef& ptx, const uint256& block_hash, int posInBlock, bool fUpdate)
1199+
bool CWallet::AddToWalletIfInvolvingMe(const CTransactionRef& ptx, CWalletTx::Status status, const uint256& block_hash, int posInBlock, bool fUpdate)
11971200
{
11981201
const CTransaction& tx = *ptx;
11991202
{
@@ -1240,9 +1243,9 @@ bool CWallet::AddToWalletIfInvolvingMe(const CTransactionRef& ptx, const uint256
12401243

12411244
CWalletTx wtx(this, ptx);
12421245

1243-
// Get merkle branch if transaction was found in a block
1244-
if (!block_hash.IsNull())
1245-
wtx.SetMerkleBranch(block_hash, posInBlock);
1246+
// Block disconnection override an abandoned tx as unconfirmed
1247+
// which means user may have to call abandontransaction again
1248+
wtx.SetConf(status, block_hash, posInBlock);
12461249

12471250
return AddToWallet(wtx, false);
12481251
}
@@ -1302,7 +1305,7 @@ bool CWallet::AbandonTransaction(interfaces::Chain::Lock& locked_chain, const ui
13021305
if (currentconfirm == 0 && !wtx.isAbandoned()) {
13031306
// If the orig tx was not in block/mempool, none of its spends can be in mempool
13041307
assert(!wtx.InMempool());
1305-
wtx.nIndex = -1;
1308+
wtx.m_confirm.nIndex = 0;
13061309
wtx.setAbandoned();
13071310
wtx.MarkDirty();
13081311
batch.WriteTx(wtx);
@@ -1356,8 +1359,9 @@ void CWallet::MarkConflicted(const uint256& hashBlock, const uint256& hashTx)
13561359
if (conflictconfirms < currentconfirm) {
13571360
// Block is 'more conflicted' than current confirm; update.
13581361
// Mark transaction as conflicted with this block.
1359-
wtx.nIndex = -1;
1360-
wtx.hashBlock = hashBlock;
1362+
wtx.m_confirm.nIndex = 0;
1363+
wtx.m_confirm.hashBlock = hashBlock;
1364+
wtx.setConflicted();
13611365
wtx.MarkDirty();
13621366
batch.WriteTx(wtx);
13631367
// Iterate over all its outputs, and mark transactions in the wallet that spend them conflicted too
@@ -1375,8 +1379,9 @@ void CWallet::MarkConflicted(const uint256& hashBlock, const uint256& hashTx)
13751379
}
13761380
}
13771381

1378-
void CWallet::SyncTransaction(const CTransactionRef& ptx, const uint256& block_hash, int posInBlock, bool update_tx) {
1379-
if (!AddToWalletIfInvolvingMe(ptx, block_hash, posInBlock, update_tx))
1382+
void CWallet::SyncTransaction(const CTransactionRef& ptx, CWalletTx::Status status, const uint256& block_hash, int posInBlock, bool update_tx)
1383+
{
1384+
if (!AddToWalletIfInvolvingMe(ptx, status, block_hash, posInBlock, update_tx))
13801385
return; // Not one of ours
13811386

13821387
// If a transaction changes 'conflicted' state, that changes the balance
@@ -1388,7 +1393,7 @@ void CWallet::SyncTransaction(const CTransactionRef& ptx, const uint256& block_h
13881393
void CWallet::TransactionAddedToMempool(const CTransactionRef& ptx) {
13891394
auto locked_chain = chain().lock();
13901395
LOCK(cs_wallet);
1391-
SyncTransaction(ptx, {} /* block hash */, 0 /* position in block */);
1396+
SyncTransaction(ptx, CWalletTx::Status::UNCONFIRMED, {} /* block hash */, 0 /* position in block */);
13921397

13931398
auto it = mapWallet.find(ptx->GetHash());
13941399
if (it != mapWallet.end()) {
@@ -1408,22 +1413,14 @@ void CWallet::BlockConnected(const CBlock& block, const std::vector<CTransaction
14081413
const uint256& block_hash = block.GetHash();
14091414
auto locked_chain = chain().lock();
14101415
LOCK(cs_wallet);
1411-
// TODO: Temporarily ensure that mempool removals are notified before
1412-
// connected transactions. This shouldn't matter, but the abandoned
1413-
// state of transactions in our wallet is currently cleared when we
1414-
// receive another notification and there is a race condition where
1415-
// notification of a connected conflict might cause an outside process
1416-
// to abandon a transaction and then have it inadvertently cleared by
1417-
// the notification that the conflicted transaction was evicted.
14181416

1419-
for (const CTransactionRef& ptx : vtxConflicted) {
1420-
SyncTransaction(ptx, {} /* block hash */, 0 /* position in block */);
1421-
TransactionRemovedFromMempool(ptx);
1422-
}
14231417
for (size_t i = 0; i < block.vtx.size(); i++) {
1424-
SyncTransaction(block.vtx[i], block_hash, i);
1418+
SyncTransaction(block.vtx[i], CWalletTx::Status::CONFIRMED, block_hash, i);
14251419
TransactionRemovedFromMempool(block.vtx[i]);
14261420
}
1421+
for (const CTransactionRef& ptx : vtxConflicted) {
1422+
TransactionRemovedFromMempool(ptx);
1423+
}
14271424

14281425
m_last_block_processed = block_hash;
14291426
}
@@ -1432,8 +1429,12 @@ void CWallet::BlockDisconnected(const CBlock& block) {
14321429
auto locked_chain = chain().lock();
14331430
LOCK(cs_wallet);
14341431

1432+
// At block disconnection, this will change an abandoned transaction to
1433+
// be unconfirmed, whether or not the transaction is added back to the mempool.
1434+
// User may have to call abandontransaction again. It may be addressed in the
1435+
// future with a stickier abandoned state or even removing abandontransaction call.
14351436
for (const CTransactionRef& ptx : block.vtx) {
1436-
SyncTransaction(ptx, {} /* block hash */, 0 /* position in block */);
1437+
SyncTransaction(ptx, CWalletTx::Status::UNCONFIRMED, {} /* block hash */, 0 /* position in block */);
14371438
}
14381439
}
14391440

@@ -2070,7 +2071,7 @@ CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_bloc
20702071
break;
20712072
}
20722073
for (size_t posInBlock = 0; posInBlock < block.vtx.size(); ++posInBlock) {
2073-
SyncTransaction(block.vtx[posInBlock], block_hash, posInBlock, fUpdate);
2074+
SyncTransaction(block.vtx[posInBlock], CWalletTx::Status::CONFIRMED, block_hash, posInBlock, fUpdate);
20742075
}
20752076
// scan succeeded, record block as most recent successfully scanned
20762077
result.last_scanned_block = block_hash;
@@ -3332,6 +3333,11 @@ bool CWallet::CommitTransaction(CTransactionRef tx, mapValue_t mapValue, std::ve
33323333

33333334
DBErrors CWallet::LoadWallet(bool& fFirstRunRet)
33343335
{
3336+
// Even if we don't use this lock in this function, we want to preserve
3337+
// lock order in LoadToWallet if query of chain state is needed to know
3338+
// tx status. If lock can't be taken (e.g wallet-tool), tx confirmation
3339+
// status may be not reliable.
3340+
auto locked_chain = LockChain();
33353341
LOCK(cs_wallet);
33363342

33373343
fFirstRunRet = false;
@@ -4042,7 +4048,7 @@ void CWallet::GetKeyBirthTimes(interfaces::Chain::Lock& locked_chain, std::map<C
40424048
for (const auto& entry : mapWallet) {
40434049
// iterate over all wallet transactions...
40444050
const CWalletTx &wtx = entry.second;
4045-
if (Optional<int> height = locked_chain.getBlockHeight(wtx.hashBlock)) {
4051+
if (Optional<int> height = locked_chain.getBlockHeight(wtx.m_confirm.hashBlock)) {
40464052
// ... which are already in a block
40474053
for (const CTxOut &txout : wtx.tx->vout) {
40484054
// iterate over all their outputs
@@ -4085,9 +4091,9 @@ void CWallet::GetKeyBirthTimes(interfaces::Chain::Lock& locked_chain, std::map<C
40854091
unsigned int CWallet::ComputeTimeSmart(const CWalletTx& wtx) const
40864092
{
40874093
unsigned int nTimeSmart = wtx.nTimeReceived;
4088-
if (!wtx.hashUnset()) {
4094+
if (!wtx.isUnconfirmed() && !wtx.isAbandoned()) {
40894095
int64_t blocktime;
4090-
if (chain().findBlock(wtx.hashBlock, nullptr /* block */, &blocktime)) {
4096+
if (chain().findBlock(wtx.m_confirm.hashBlock, nullptr /* block */, &blocktime)) {
40914097
int64_t latestNow = wtx.nTimeReceived;
40924098
int64_t latestEntry = 0;
40934099

@@ -4115,7 +4121,7 @@ unsigned int CWallet::ComputeTimeSmart(const CWalletTx& wtx) const
41154121

41164122
nTimeSmart = std::max(latestEntry, std::min(blocktime, latestNow));
41174123
} else {
4118-
WalletLogPrintf("%s: found %s in block %s not in index\n", __func__, wtx.GetHash().ToString(), wtx.hashBlock.ToString());
4124+
WalletLogPrintf("%s: found %s in block %s not in index\n", __func__, wtx.GetHash().ToString(), wtx.m_confirm.hashBlock.ToString());
41194125
}
41204126
}
41214127
return nTimeSmart;
@@ -4233,6 +4239,11 @@ bool CWallet::Verify(interfaces::Chain& chain, const WalletLocation& location, b
42334239
// Recover readable keypairs:
42344240
CWallet dummyWallet(&chain, WalletLocation(), WalletDatabase::CreateDummy());
42354241
std::string backup_filename;
4242+
// Even if we don't use this lock in this function, we want to preserve
4243+
// lock order in LoadToWallet if query of chain state is needed to know
4244+
// tx status. If lock can't be taken, tx confirmation status may be not
4245+
// reliable.
4246+
auto locked_chain = dummyWallet.LockChain();
42364247
if (!WalletBatch::Recover(wallet_path, (void *)&dummyWallet, WalletBatch::RecoverKeysOnlyFilter, backup_filename)) {
42374248
return false;
42384249
}
@@ -4627,21 +4638,23 @@ CKeyPool::CKeyPool(const CPubKey& vchPubKeyIn, bool internalIn)
46274638
m_pre_split = false;
46284639
}
46294640

4630-
void CWalletTx::SetMerkleBranch(const uint256& block_hash, int posInBlock)
4641+
void CWalletTx::SetConf(Status status, const uint256& block_hash, int posInBlock)
46314642
{
4643+
// Update tx status
4644+
m_confirm.status = status;
4645+
46324646
// Update the tx's hashBlock
4633-
hashBlock = block_hash;
4647+
m_confirm.hashBlock = block_hash;
46344648

46354649
// set the position of the transaction in the block
4636-
nIndex = posInBlock;
4650+
m_confirm.nIndex = posInBlock;
46374651
}
46384652

46394653
int CWalletTx::GetDepthInMainChain(interfaces::Chain::Lock& locked_chain) const
46404654
{
4641-
if (hashUnset())
4642-
return 0;
4655+
if (isUnconfirmed() || isAbandoned()) return 0;
46434656

4644-
return locked_chain.getBlockDepth(hashBlock) * (nIndex == -1 ? -1 : 1);
4657+
return locked_chain.getBlockDepth(m_confirm.hashBlock) * (isConflicted() ? -1 : 1);
46454658
}
46464659

46474660
int CWalletTx::GetBlocksToMaturity(interfaces::Chain::Lock& locked_chain) const

0 commit comments

Comments
 (0)