Merge #16624: wallet: encapsulate transactions state

442a87cc0a Add a test wallet_reorgsrestore (Antoine Riard)
40ede992d9 Modify wallet tx status if has been reorged out (Antoine Riard)
7e89994133 Remove SyncTransaction for conflicted txn in CWallet::BlockConnected (Antoine Riard)
a31be09bfd 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](https://github.com/bitcoin/bitcoin/pull/15931#discussion_r312576506), 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 :
  * https://github.com/bitcoin/bitcoin/issues/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)
  * https://github.com/bitcoin/bitcoin/issues/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 442a87cc0a
  ryanofsky:
    utACK 442a87cc0a. 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
This commit is contained in:
MeshCollider 2019-09-06 01:28:15 +12:00
commit 5e202382a9
No known key found for this signature in database
GPG key ID: D300116E1C875A3D
8 changed files with 261 additions and 87 deletions

View file

@ -65,7 +65,7 @@ WalletTx MakeWalletTx(interfaces::Chain::Lock& locked_chain, CWallet& wallet, co
WalletTxStatus MakeWalletTxStatus(interfaces::Chain::Lock& locked_chain, const CWalletTx& wtx) WalletTxStatus MakeWalletTxStatus(interfaces::Chain::Lock& locked_chain, const CWalletTx& wtx)
{ {
WalletTxStatus result; WalletTxStatus result;
result.block_height = locked_chain.getBlockHeight(wtx.hashBlock).get_value_or(std::numeric_limits<int>::max()); result.block_height = locked_chain.getBlockHeight(wtx.m_confirm.hashBlock).get_value_or(std::numeric_limits<int>::max());
result.blocks_to_maturity = wtx.GetBlocksToMaturity(locked_chain); result.blocks_to_maturity = wtx.GetBlocksToMaturity(locked_chain);
result.depth_in_main_chain = wtx.GetDepthInMainChain(locked_chain); result.depth_in_main_chain = wtx.GetDepthInMainChain(locked_chain);
result.time_received = wtx.nTimeReceived; result.time_received = wtx.nTimeReceived;

View file

@ -384,8 +384,7 @@ UniValue importprunedfunds(const JSONRPCRequest& request)
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Something wrong with merkleblock"); throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Something wrong with merkleblock");
} }
wtx.nIndex = txnIndex; wtx.SetConf(CWalletTx::Status::CONFIRMED, merkleBlock.header.GetHash(), txnIndex);
wtx.hashBlock = merkleBlock.header.GetHash();
auto locked_chain = pwallet->chain().lock(); auto locked_chain = pwallet->chain().lock();
LOCK(pwallet->cs_wallet); LOCK(pwallet->cs_wallet);

View file

@ -134,10 +134,10 @@ static void WalletTxToJSON(interfaces::Chain& chain, interfaces::Chain::Lock& lo
entry.pushKV("generated", true); entry.pushKV("generated", true);
if (confirms > 0) if (confirms > 0)
{ {
entry.pushKV("blockhash", wtx.hashBlock.GetHex()); entry.pushKV("blockhash", wtx.m_confirm.hashBlock.GetHex());
entry.pushKV("blockindex", wtx.nIndex); entry.pushKV("blockindex", wtx.m_confirm.nIndex);
int64_t block_time; int64_t block_time;
bool found_block = chain.findBlock(wtx.hashBlock, nullptr /* block */, &block_time); bool found_block = chain.findBlock(wtx.m_confirm.hashBlock, nullptr /* block */, &block_time);
assert(found_block); assert(found_block);
entry.pushKV("blocktime", block_time); entry.pushKV("blocktime", block_time);
} else { } else {

View file

@ -249,8 +249,7 @@ BOOST_FIXTURE_TEST_CASE(coin_mark_dirty_immature_credit, TestChain100Setup)
LockAssertion lock(::cs_main); LockAssertion lock(::cs_main);
LOCK(wallet.cs_wallet); LOCK(wallet.cs_wallet);
wtx.hashBlock = ::ChainActive().Tip()->GetBlockHash(); wtx.SetConf(CWalletTx::Status::CONFIRMED, ::ChainActive().Tip()->GetBlockHash(), 0);
wtx.nIndex = 0;
// Call GetImmatureCredit() once before adding the key to the wallet to // Call GetImmatureCredit() once before adding the key to the wallet to
// cache the current immature credit amount, which is 0. // 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
} }
CWalletTx wtx(&wallet, MakeTransactionRef(tx)); CWalletTx wtx(&wallet, MakeTransactionRef(tx));
if (block) {
wtx.SetMerkleBranch(block->GetBlockHash(), 0);
}
{
LOCK(cs_main); LOCK(cs_main);
LOCK(wallet.cs_wallet);
// If transaction is already in map, to avoid inconsistencies, unconfirmation
// is needed before confirm again with different block.
std::map<uint256, CWalletTx>::iterator it = wallet.mapWallet.find(wtx.GetHash());
if (it != wallet.mapWallet.end()) {
wtx.setUnconfirmed();
wallet.AddToWallet(wtx); wallet.AddToWallet(wtx);
} }
LOCK(wallet.cs_wallet); if (block) {
wtx.SetConf(CWalletTx::Status::CONFIRMED, block->GetBlockHash(), 0);
}
wallet.AddToWallet(wtx);
return wallet.mapWallet.at(wtx.GetHash()).nTimeSmart; return wallet.mapWallet.at(wtx.GetHash()).nTimeSmart;
} }
@ -382,7 +386,7 @@ public:
LOCK(wallet->cs_wallet); LOCK(wallet->cs_wallet);
auto it = wallet->mapWallet.find(tx->GetHash()); auto it = wallet->mapWallet.find(tx->GetHash());
BOOST_CHECK(it != wallet->mapWallet.end()); BOOST_CHECK(it != wallet->mapWallet.end());
it->second.SetMerkleBranch(::ChainActive().Tip()->GetBlockHash(), 1); it->second.SetConf(CWalletTx::Status::CONFIRMED, ::ChainActive().Tip()->GetBlockHash(), 1);
return it->second; return it->second;
} }

View file

@ -1110,22 +1110,14 @@ bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose)
bool fUpdated = false; bool fUpdated = false;
if (!fInsertedNew) if (!fInsertedNew)
{ {
// Merge if (wtxIn.m_confirm.status != wtx.m_confirm.status) {
if (!wtxIn.hashUnset() && wtxIn.hashBlock != wtx.hashBlock) wtx.m_confirm.status = wtxIn.m_confirm.status;
{ wtx.m_confirm.nIndex = wtxIn.m_confirm.nIndex;
wtx.hashBlock = wtxIn.hashBlock; wtx.m_confirm.hashBlock = wtxIn.m_confirm.hashBlock;
fUpdated = true;
}
// If no longer abandoned, update
if (wtxIn.hashBlock.IsNull() && wtx.isAbandoned())
{
wtx.hashBlock = wtxIn.hashBlock;
fUpdated = true;
}
if (wtxIn.nIndex != -1 && (wtxIn.nIndex != wtx.nIndex))
{
wtx.nIndex = wtxIn.nIndex;
fUpdated = true; fUpdated = true;
} else {
assert(wtx.m_confirm.nIndex == wtxIn.m_confirm.nIndex);
assert(wtx.m_confirm.hashBlock == wtxIn.m_confirm.hashBlock);
} }
if (wtxIn.fFromMe && wtxIn.fFromMe != wtx.fFromMe) if (wtxIn.fFromMe && wtxIn.fFromMe != wtx.fFromMe)
{ {
@ -1172,8 +1164,19 @@ bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose)
return true; return true;
} }
void CWallet::LoadToWallet(const CWalletTx& wtxIn) void CWallet::LoadToWallet(CWalletTx& wtxIn)
{ {
// If wallet doesn't have a chain (e.g wallet-tool), lock can't be taken.
auto locked_chain = LockChain();
// If tx hasn't been reorged out of chain while wallet being shutdown
// change tx status to UNCONFIRMED and reset hashBlock/nIndex.
if (!wtxIn.m_confirm.hashBlock.IsNull()) {
if (locked_chain && !locked_chain->getBlockHeight(wtxIn.m_confirm.hashBlock)) {
wtxIn.setUnconfirmed();
wtxIn.m_confirm.hashBlock = uint256();
wtxIn.m_confirm.nIndex = 0;
}
}
uint256 hash = wtxIn.GetHash(); uint256 hash = wtxIn.GetHash();
const auto& ins = mapWallet.emplace(hash, wtxIn); const auto& ins = mapWallet.emplace(hash, wtxIn);
CWalletTx& wtx = ins.first->second; CWalletTx& wtx = ins.first->second;
@ -1186,14 +1189,14 @@ void CWallet::LoadToWallet(const CWalletTx& wtxIn)
auto it = mapWallet.find(txin.prevout.hash); auto it = mapWallet.find(txin.prevout.hash);
if (it != mapWallet.end()) { if (it != mapWallet.end()) {
CWalletTx& prevtx = it->second; CWalletTx& prevtx = it->second;
if (prevtx.nIndex == -1 && !prevtx.hashUnset()) { if (prevtx.isConflicted()) {
MarkConflicted(prevtx.hashBlock, wtx.GetHash()); MarkConflicted(prevtx.m_confirm.hashBlock, wtx.GetHash());
} }
} }
} }
} }
bool CWallet::AddToWalletIfInvolvingMe(const CTransactionRef& ptx, const uint256& block_hash, int posInBlock, bool fUpdate) bool CWallet::AddToWalletIfInvolvingMe(const CTransactionRef& ptx, CWalletTx::Status status, const uint256& block_hash, int posInBlock, bool fUpdate)
{ {
const CTransaction& tx = *ptx; const CTransaction& tx = *ptx;
{ {
@ -1240,9 +1243,9 @@ bool CWallet::AddToWalletIfInvolvingMe(const CTransactionRef& ptx, const uint256
CWalletTx wtx(this, ptx); CWalletTx wtx(this, ptx);
// Get merkle branch if transaction was found in a block // Block disconnection override an abandoned tx as unconfirmed
if (!block_hash.IsNull()) // which means user may have to call abandontransaction again
wtx.SetMerkleBranch(block_hash, posInBlock); wtx.SetConf(status, block_hash, posInBlock);
return AddToWallet(wtx, false); return AddToWallet(wtx, false);
} }
@ -1302,7 +1305,7 @@ bool CWallet::AbandonTransaction(interfaces::Chain::Lock& locked_chain, const ui
if (currentconfirm == 0 && !wtx.isAbandoned()) { if (currentconfirm == 0 && !wtx.isAbandoned()) {
// If the orig tx was not in block/mempool, none of its spends can be in mempool // If the orig tx was not in block/mempool, none of its spends can be in mempool
assert(!wtx.InMempool()); assert(!wtx.InMempool());
wtx.nIndex = -1; wtx.m_confirm.nIndex = 0;
wtx.setAbandoned(); wtx.setAbandoned();
wtx.MarkDirty(); wtx.MarkDirty();
batch.WriteTx(wtx); batch.WriteTx(wtx);
@ -1356,8 +1359,9 @@ void CWallet::MarkConflicted(const uint256& hashBlock, const uint256& hashTx)
if (conflictconfirms < currentconfirm) { if (conflictconfirms < currentconfirm) {
// Block is 'more conflicted' than current confirm; update. // Block is 'more conflicted' than current confirm; update.
// Mark transaction as conflicted with this block. // Mark transaction as conflicted with this block.
wtx.nIndex = -1; wtx.m_confirm.nIndex = 0;
wtx.hashBlock = hashBlock; wtx.m_confirm.hashBlock = hashBlock;
wtx.setConflicted();
wtx.MarkDirty(); wtx.MarkDirty();
batch.WriteTx(wtx); batch.WriteTx(wtx);
// Iterate over all its outputs, and mark transactions in the wallet that spend them conflicted too // 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)
} }
} }
void CWallet::SyncTransaction(const CTransactionRef& ptx, const uint256& block_hash, int posInBlock, bool update_tx) { void CWallet::SyncTransaction(const CTransactionRef& ptx, CWalletTx::Status status, const uint256& block_hash, int posInBlock, bool update_tx)
if (!AddToWalletIfInvolvingMe(ptx, block_hash, posInBlock, update_tx)) {
if (!AddToWalletIfInvolvingMe(ptx, status, block_hash, posInBlock, update_tx))
return; // Not one of ours return; // Not one of ours
// If a transaction changes 'conflicted' state, that changes the balance // If a transaction changes 'conflicted' state, that changes the balance
@ -1388,7 +1393,7 @@ void CWallet::SyncTransaction(const CTransactionRef& ptx, const uint256& block_h
void CWallet::TransactionAddedToMempool(const CTransactionRef& ptx) { void CWallet::TransactionAddedToMempool(const CTransactionRef& ptx) {
auto locked_chain = chain().lock(); auto locked_chain = chain().lock();
LOCK(cs_wallet); LOCK(cs_wallet);
SyncTransaction(ptx, {} /* block hash */, 0 /* position in block */); SyncTransaction(ptx, CWalletTx::Status::UNCONFIRMED, {} /* block hash */, 0 /* position in block */);
auto it = mapWallet.find(ptx->GetHash()); auto it = mapWallet.find(ptx->GetHash());
if (it != mapWallet.end()) { if (it != mapWallet.end()) {
@ -1408,22 +1413,14 @@ void CWallet::BlockConnected(const CBlock& block, const std::vector<CTransaction
const uint256& block_hash = block.GetHash(); const uint256& block_hash = block.GetHash();
auto locked_chain = chain().lock(); auto locked_chain = chain().lock();
LOCK(cs_wallet); LOCK(cs_wallet);
// TODO: Temporarily ensure that mempool removals are notified before
// connected transactions. This shouldn't matter, but the abandoned
// state of transactions in our wallet is currently cleared when we
// receive another notification and there is a race condition where
// notification of a connected conflict might cause an outside process
// to abandon a transaction and then have it inadvertently cleared by
// the notification that the conflicted transaction was evicted.
for (const CTransactionRef& ptx : vtxConflicted) {
SyncTransaction(ptx, {} /* block hash */, 0 /* position in block */);
TransactionRemovedFromMempool(ptx);
}
for (size_t i = 0; i < block.vtx.size(); i++) { for (size_t i = 0; i < block.vtx.size(); i++) {
SyncTransaction(block.vtx[i], block_hash, i); SyncTransaction(block.vtx[i], CWalletTx::Status::CONFIRMED, block_hash, i);
TransactionRemovedFromMempool(block.vtx[i]); TransactionRemovedFromMempool(block.vtx[i]);
} }
for (const CTransactionRef& ptx : vtxConflicted) {
TransactionRemovedFromMempool(ptx);
}
m_last_block_processed = block_hash; m_last_block_processed = block_hash;
} }
@ -1432,8 +1429,12 @@ void CWallet::BlockDisconnected(const CBlock& block) {
auto locked_chain = chain().lock(); auto locked_chain = chain().lock();
LOCK(cs_wallet); LOCK(cs_wallet);
// At block disconnection, this will change an abandoned transaction to
// be unconfirmed, whether or not the transaction is added back to the mempool.
// User may have to call abandontransaction again. It may be addressed in the
// future with a stickier abandoned state or even removing abandontransaction call.
for (const CTransactionRef& ptx : block.vtx) { for (const CTransactionRef& ptx : block.vtx) {
SyncTransaction(ptx, {} /* block hash */, 0 /* position in block */); SyncTransaction(ptx, CWalletTx::Status::UNCONFIRMED, {} /* block hash */, 0 /* position in block */);
} }
} }
@ -2070,7 +2071,7 @@ CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_bloc
break; break;
} }
for (size_t posInBlock = 0; posInBlock < block.vtx.size(); ++posInBlock) { for (size_t posInBlock = 0; posInBlock < block.vtx.size(); ++posInBlock) {
SyncTransaction(block.vtx[posInBlock], block_hash, posInBlock, fUpdate); SyncTransaction(block.vtx[posInBlock], CWalletTx::Status::CONFIRMED, block_hash, posInBlock, fUpdate);
} }
// scan succeeded, record block as most recent successfully scanned // scan succeeded, record block as most recent successfully scanned
result.last_scanned_block = block_hash; result.last_scanned_block = block_hash;
@ -3332,6 +3333,11 @@ bool CWallet::CommitTransaction(CTransactionRef tx, mapValue_t mapValue, std::ve
DBErrors CWallet::LoadWallet(bool& fFirstRunRet) DBErrors CWallet::LoadWallet(bool& fFirstRunRet)
{ {
// Even if we don't use this lock in this function, we want to preserve
// lock order in LoadToWallet if query of chain state is needed to know
// tx status. If lock can't be taken (e.g wallet-tool), tx confirmation
// status may be not reliable.
auto locked_chain = LockChain();
LOCK(cs_wallet); LOCK(cs_wallet);
fFirstRunRet = false; fFirstRunRet = false;
@ -4042,7 +4048,7 @@ void CWallet::GetKeyBirthTimes(interfaces::Chain::Lock& locked_chain, std::map<C
for (const auto& entry : mapWallet) { for (const auto& entry : mapWallet) {
// iterate over all wallet transactions... // iterate over all wallet transactions...
const CWalletTx &wtx = entry.second; const CWalletTx &wtx = entry.second;
if (Optional<int> height = locked_chain.getBlockHeight(wtx.hashBlock)) { if (Optional<int> height = locked_chain.getBlockHeight(wtx.m_confirm.hashBlock)) {
// ... which are already in a block // ... which are already in a block
for (const CTxOut &txout : wtx.tx->vout) { for (const CTxOut &txout : wtx.tx->vout) {
// iterate over all their outputs // iterate over all their outputs
@ -4085,9 +4091,9 @@ void CWallet::GetKeyBirthTimes(interfaces::Chain::Lock& locked_chain, std::map<C
unsigned int CWallet::ComputeTimeSmart(const CWalletTx& wtx) const unsigned int CWallet::ComputeTimeSmart(const CWalletTx& wtx) const
{ {
unsigned int nTimeSmart = wtx.nTimeReceived; unsigned int nTimeSmart = wtx.nTimeReceived;
if (!wtx.hashUnset()) { if (!wtx.isUnconfirmed() && !wtx.isAbandoned()) {
int64_t blocktime; int64_t blocktime;
if (chain().findBlock(wtx.hashBlock, nullptr /* block */, &blocktime)) { if (chain().findBlock(wtx.m_confirm.hashBlock, nullptr /* block */, &blocktime)) {
int64_t latestNow = wtx.nTimeReceived; int64_t latestNow = wtx.nTimeReceived;
int64_t latestEntry = 0; int64_t latestEntry = 0;
@ -4115,7 +4121,7 @@ unsigned int CWallet::ComputeTimeSmart(const CWalletTx& wtx) const
nTimeSmart = std::max(latestEntry, std::min(blocktime, latestNow)); nTimeSmart = std::max(latestEntry, std::min(blocktime, latestNow));
} else { } else {
WalletLogPrintf("%s: found %s in block %s not in index\n", __func__, wtx.GetHash().ToString(), wtx.hashBlock.ToString()); WalletLogPrintf("%s: found %s in block %s not in index\n", __func__, wtx.GetHash().ToString(), wtx.m_confirm.hashBlock.ToString());
} }
} }
return nTimeSmart; return nTimeSmart;
@ -4233,6 +4239,11 @@ bool CWallet::Verify(interfaces::Chain& chain, const WalletLocation& location, b
// Recover readable keypairs: // Recover readable keypairs:
CWallet dummyWallet(&chain, WalletLocation(), WalletDatabase::CreateDummy()); CWallet dummyWallet(&chain, WalletLocation(), WalletDatabase::CreateDummy());
std::string backup_filename; std::string backup_filename;
// Even if we don't use this lock in this function, we want to preserve
// lock order in LoadToWallet if query of chain state is needed to know
// tx status. If lock can't be taken, tx confirmation status may be not
// reliable.
auto locked_chain = dummyWallet.LockChain();
if (!WalletBatch::Recover(wallet_path, (void *)&dummyWallet, WalletBatch::RecoverKeysOnlyFilter, backup_filename)) { if (!WalletBatch::Recover(wallet_path, (void *)&dummyWallet, WalletBatch::RecoverKeysOnlyFilter, backup_filename)) {
return false; return false;
} }
@ -4627,21 +4638,23 @@ CKeyPool::CKeyPool(const CPubKey& vchPubKeyIn, bool internalIn)
m_pre_split = false; m_pre_split = false;
} }
void CWalletTx::SetMerkleBranch(const uint256& block_hash, int posInBlock) void CWalletTx::SetConf(Status status, const uint256& block_hash, int posInBlock)
{ {
// Update tx status
m_confirm.status = status;
// Update the tx's hashBlock // Update the tx's hashBlock
hashBlock = block_hash; m_confirm.hashBlock = block_hash;
// set the position of the transaction in the block // set the position of the transaction in the block
nIndex = posInBlock; m_confirm.nIndex = posInBlock;
} }
int CWalletTx::GetDepthInMainChain(interfaces::Chain::Lock& locked_chain) const int CWalletTx::GetDepthInMainChain(interfaces::Chain::Lock& locked_chain) const
{ {
if (hashUnset()) if (isUnconfirmed() || isAbandoned()) return 0;
return 0;
return locked_chain.getBlockDepth(hashBlock) * (nIndex == -1 ? -1 : 1); return locked_chain.getBlockDepth(m_confirm.hashBlock) * (isConflicted() ? -1 : 1);
} }
int CWalletTx::GetBlocksToMaturity(interfaces::Chain::Lock& locked_chain) const int CWalletTx::GetBlocksToMaturity(interfaces::Chain::Lock& locked_chain) const

View file

@ -396,7 +396,9 @@ class CWalletTx
private: private:
const CWallet* pwallet; const CWallet* pwallet;
/** Constant used in hashBlock to indicate tx has been abandoned */ /** Constant used in hashBlock to indicate tx has been abandoned, only used at
* serialization/deserialization to avoid ambiguity with conflicted.
*/
static const uint256 ABANDON_HASH; static const uint256 ABANDON_HASH;
public: public:
@ -457,9 +459,7 @@ public:
mutable CAmount nChangeCached; mutable CAmount nChangeCached;
CWalletTx(const CWallet* pwalletIn, CTransactionRef arg) CWalletTx(const CWallet* pwalletIn, CTransactionRef arg)
: tx(std::move(arg)), : tx(std::move(arg))
hashBlock(uint256()),
nIndex(-1)
{ {
Init(pwalletIn); Init(pwalletIn);
} }
@ -477,16 +477,37 @@ public:
fInMempool = false; fInMempool = false;
nChangeCached = 0; nChangeCached = 0;
nOrderPos = -1; nOrderPos = -1;
m_confirm = Confirmation{};
} }
CTransactionRef tx; CTransactionRef tx;
uint256 hashBlock;
/* An nIndex == -1 means that hashBlock (in nonzero) refers to the earliest /* New transactions start as UNCONFIRMED. At BlockConnected,
* block in the chain we know this or any in-wallet dependency conflicts * they will transition to CONFIRMED. In case of reorg, at BlockDisconnected,
* with. Older clients interpret nIndex == -1 as unconfirmed for backward * they roll back to UNCONFIRMED. If we detect a conflicting transaction at
* compatibility. * block connection, we update conflicted tx and its dependencies as CONFLICTED.
* If tx isn't confirmed and outside of mempool, the user may switch it to ABANDONED
* by using the abandontransaction call. This last status may be override by a CONFLICTED
* or CONFIRMED transition.
*/ */
int nIndex; enum Status {
UNCONFIRMED,
CONFIRMED,
CONFLICTED,
ABANDONED
};
/* Confirmation includes tx status and a pair of {block hash/tx index in block} at which tx has been confirmed.
* This pair is both 0 if tx hasn't confirmed yet. Meaning of these fields changes with CONFLICTED state
* where they instead point to block hash and index of the deepest conflicting tx.
*/
struct Confirmation {
Status status = UNCONFIRMED;
uint256 hashBlock = uint256();
int nIndex = 0;
};
Confirmation m_confirm;
template<typename Stream> template<typename Stream>
void Serialize(Stream& s) const void Serialize(Stream& s) const
@ -502,7 +523,9 @@ public:
std::vector<char> dummy_vector1; //!< Used to be vMerkleBranch std::vector<char> dummy_vector1; //!< Used to be vMerkleBranch
std::vector<char> dummy_vector2; //!< Used to be vtxPrev std::vector<char> dummy_vector2; //!< Used to be vtxPrev
bool dummy_bool = false; //!< Used to be fSpent bool dummy_bool = false; //!< Used to be fSpent
s << tx << hashBlock << dummy_vector1 << nIndex << dummy_vector2 << mapValueCopy << vOrderForm << fTimeReceivedIsTxTime << nTimeReceived << fFromMe << dummy_bool; uint256 serializedHash = isAbandoned() ? ABANDON_HASH : m_confirm.hashBlock;
int serializedIndex = isAbandoned() || isConflicted() ? -1 : m_confirm.nIndex;
s << tx << serializedHash << dummy_vector1 << serializedIndex << dummy_vector2 << mapValueCopy << vOrderForm << fTimeReceivedIsTxTime << nTimeReceived << fFromMe << dummy_bool;
} }
template<typename Stream> template<typename Stream>
@ -513,7 +536,25 @@ public:
std::vector<uint256> dummy_vector1; //!< Used to be vMerkleBranch std::vector<uint256> dummy_vector1; //!< Used to be vMerkleBranch
std::vector<CMerkleTx> dummy_vector2; //!< Used to be vtxPrev std::vector<CMerkleTx> dummy_vector2; //!< Used to be vtxPrev
bool dummy_bool; //! Used to be fSpent bool dummy_bool; //! Used to be fSpent
s >> tx >> hashBlock >> dummy_vector1 >> nIndex >> dummy_vector2 >> mapValue >> vOrderForm >> fTimeReceivedIsTxTime >> nTimeReceived >> fFromMe >> dummy_bool; int serializedIndex;
s >> tx >> m_confirm.hashBlock >> dummy_vector1 >> serializedIndex >> dummy_vector2 >> mapValue >> vOrderForm >> fTimeReceivedIsTxTime >> nTimeReceived >> fFromMe >> dummy_bool;
/* At serialization/deserialization, an nIndex == -1 means that hashBlock refers to
* the earliest block in the chain we know this or any in-wallet ancestor conflicts
* with. If nIndex == -1 and hashBlock is ABANDON_HASH, it means transaction is abandoned.
* In same context, an nIndex >= 0 refers to a confirmed transaction (if hashBlock set) or
* unconfirmed one. Older clients interpret nIndex == -1 as unconfirmed for backward
* compatibility (pre-commit 9ac63d6).
*/
if (serializedIndex == -1 && m_confirm.hashBlock == ABANDON_HASH) {
m_confirm.hashBlock = uint256();
setAbandoned();
} else if (serializedIndex == -1) {
setConflicted();
} else if (!m_confirm.hashBlock.IsNull()) {
m_confirm.nIndex = serializedIndex;
setConfirmed();
}
ReadOrderPos(nOrderPos, mapValue); ReadOrderPos(nOrderPos, mapValue);
nTimeSmart = mapValue.count("timesmart") ? (unsigned int)atoi64(mapValue["timesmart"]) : 0; nTimeSmart = mapValue.count("timesmart") ? (unsigned int)atoi64(mapValue["timesmart"]) : 0;
@ -590,7 +631,7 @@ public:
// in place. // in place.
std::set<uint256> GetConflicts() const NO_THREAD_SAFETY_ANALYSIS; std::set<uint256> GetConflicts() const NO_THREAD_SAFETY_ANALYSIS;
void SetMerkleBranch(const uint256& block_hash, int posInBlock); void SetConf(Status status, const uint256& block_hash, int posInBlock);
/** /**
* Return depth of transaction in blockchain: * Return depth of transaction in blockchain:
@ -607,10 +648,18 @@ public:
* >0 : is a coinbase transaction which matures in this many blocks * >0 : is a coinbase transaction which matures in this many blocks
*/ */
int GetBlocksToMaturity(interfaces::Chain::Lock& locked_chain) const; int GetBlocksToMaturity(interfaces::Chain::Lock& locked_chain) const;
bool hashUnset() const { return (hashBlock.IsNull() || hashBlock == ABANDON_HASH); } bool isAbandoned() const { return m_confirm.status == CWalletTx::ABANDONED; }
bool isAbandoned() const { return (hashBlock == ABANDON_HASH); } void setAbandoned()
void setAbandoned() { hashBlock = ABANDON_HASH; } {
m_confirm.status = CWalletTx::ABANDONED;
m_confirm.hashBlock = uint256();
m_confirm.nIndex = 0;
}
bool isConflicted() const { return m_confirm.status == CWalletTx::CONFLICTED; }
void setConflicted() { m_confirm.status = CWalletTx::CONFLICTED; }
bool isUnconfirmed() const { return m_confirm.status == CWalletTx::UNCONFIRMED; }
void setUnconfirmed() { m_confirm.status = CWalletTx::UNCONFIRMED; }
void setConfirmed() { m_confirm.status = CWalletTx::CONFIRMED; }
const uint256& GetHash() const { return tx->GetHash(); } const uint256& GetHash() const { return tx->GetHash(); }
bool IsCoinBase() const { return tx->IsCoinBase(); } bool IsCoinBase() const { return tx->IsCoinBase(); }
bool IsImmatureCoinBase(interfaces::Chain::Lock& locked_chain) const; bool IsImmatureCoinBase(interfaces::Chain::Lock& locked_chain) const;
@ -750,7 +799,7 @@ private:
* Abandoned state should probably be more carefully tracked via different * Abandoned state should probably be more carefully tracked via different
* posInBlock signals or by checking mempool presence when necessary. * posInBlock signals or by checking mempool presence when necessary.
*/ */
bool AddToWalletIfInvolvingMe(const CTransactionRef& tx, const uint256& block_hash, int posInBlock, bool fUpdate) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool AddToWalletIfInvolvingMe(const CTransactionRef& tx, CWalletTx::Status status, const uint256& block_hash, int posInBlock, bool fUpdate) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
/* Mark a transaction (and its in-wallet descendants) as conflicting with a particular block. */ /* Mark a transaction (and its in-wallet descendants) as conflicting with a particular block. */
void MarkConflicted(const uint256& hashBlock, const uint256& hashTx); void MarkConflicted(const uint256& hashBlock, const uint256& hashTx);
@ -762,7 +811,7 @@ private:
/* Used by TransactionAddedToMemorypool/BlockConnected/Disconnected/ScanForWalletTransactions. /* Used by TransactionAddedToMemorypool/BlockConnected/Disconnected/ScanForWalletTransactions.
* Should be called with non-zero block_hash and posInBlock if this is for a transaction that is included in a block. */ * Should be called with non-zero block_hash and posInBlock if this is for a transaction that is included in a block. */
void SyncTransaction(const CTransactionRef& tx, const uint256& block_hash, int posInBlock = 0, bool update_tx = true) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void SyncTransaction(const CTransactionRef& tx, CWalletTx::Status status, const uint256& block_hash, int posInBlock = 0, bool update_tx = true) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
/* the HD chain data model (external chain counters) */ /* the HD chain data model (external chain counters) */
CHDChain hdChain; CHDChain hdChain;
@ -897,6 +946,9 @@ public:
bool IsLocked() const; bool IsLocked() const;
bool Lock(); bool Lock();
/** Interface to assert chain access and if successful lock it */
std::unique_ptr<interfaces::Chain::Lock> LockChain() { return m_chain ? m_chain->lock() : nullptr; }
std::map<uint256, CWalletTx> mapWallet GUARDED_BY(cs_wallet); std::map<uint256, CWalletTx> mapWallet GUARDED_BY(cs_wallet);
typedef std::multimap<int64_t, CWalletTx*> TxItems; typedef std::multimap<int64_t, CWalletTx*> TxItems;
@ -1042,7 +1094,7 @@ public:
void MarkDirty(); void MarkDirty();
bool AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose=true); bool AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose=true);
void LoadToWallet(const CWalletTx& wtxIn) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void LoadToWallet(CWalletTx& wtxIn) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
void TransactionAddedToMempool(const CTransactionRef& tx) override; void TransactionAddedToMempool(const CTransactionRef& tx) override;
void BlockConnected(const CBlock& block, const std::vector<CTransactionRef>& vtxConflicted) override; void BlockConnected(const CBlock& block, const std::vector<CTransactionRef>& vtxConflicted) override;
void BlockDisconnected(const CBlock& block) override; void BlockDisconnected(const CBlock& block) override;

View file

@ -131,6 +131,7 @@ BASE_SCRIPTS = [
'wallet_createwallet.py --usecli', 'wallet_createwallet.py --usecli',
'wallet_watchonly.py', 'wallet_watchonly.py',
'wallet_watchonly.py --usecli', 'wallet_watchonly.py --usecli',
'wallet_reorgsrestore.py',
'interface_http.py', 'interface_http.py',
'interface_rpc.py', 'interface_rpc.py',
'rpc_psbt.py', 'rpc_psbt.py',

View file

@ -0,0 +1,105 @@
#!/usr/bin/env python3
# Copyright (c) 2019 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test tx status in case of reorgs while wallet being shutdown.
Wallet txn status rely on block connection/disconnection for its
accuracy. In case of reorgs happening while wallet being shutdown
block updates are not going to be received. At wallet loading, we
check against chain if confirmed txn are still in chain and change
their status if block in which they have been included has been
disconnected.
"""
from decimal import Decimal
import os
import shutil
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
assert_equal,
connect_nodes,
disconnect_nodes,
)
class ReorgsRestoreTest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 3
def skip_test_if_missing_module(self):
self.skip_if_no_wallet()
def run_test(self):
# Send a tx from which to conflict outputs later
txid_conflict_from = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10"))
self.nodes[0].generate(1)
self.sync_blocks()
# Disconnect node1 from others to reorg its chain later
disconnect_nodes(self.nodes[0], 1)
disconnect_nodes(self.nodes[1], 2)
connect_nodes(self.nodes[0], 2)
# Send a tx to be unconfirmed later
txid = self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), Decimal("10"))
tx = self.nodes[0].gettransaction(txid)
self.nodes[0].generate(4)
tx_before_reorg = self.nodes[0].gettransaction(txid)
assert_equal(tx_before_reorg["confirmations"], 4)
# Disconnect node0 from node2 to broadcast a conflict on their respective chains
disconnect_nodes(self.nodes[0], 2)
nA = next(tx_out["vout"] for tx_out in self.nodes[0].gettransaction(txid_conflict_from)["details"] if tx_out["amount"] == Decimal("10"))
inputs = []
inputs.append({"txid": txid_conflict_from, "vout": nA})
outputs_1 = {}
outputs_2 = {}
# Create a conflicted tx broadcast on node0 chain and conflicting tx broadcast on node1 chain. Both spend from txid_conflict_from
outputs_1[self.nodes[0].getnewaddress()] = Decimal("9.99998")
outputs_2[self.nodes[0].getnewaddress()] = Decimal("9.99998")
conflicted = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs, outputs_1))
conflicting = self.nodes[0].signrawtransactionwithwallet(self.nodes[0].createrawtransaction(inputs, outputs_2))
conflicted_txid = self.nodes[0].sendrawtransaction(conflicted["hex"])
self.nodes[0].generate(1)
conflicting_txid = self.nodes[2].sendrawtransaction(conflicting["hex"])
self.nodes[2].generate(9)
# Reconnect node0 and node2 and check that conflicted_txid is effectively conflicted
connect_nodes(self.nodes[0], 2)
self.sync_blocks([self.nodes[0], self.nodes[2]])
conflicted = self.nodes[0].gettransaction(conflicted_txid)
conflicting = self.nodes[0].gettransaction(conflicting_txid)
assert_equal(conflicted["confirmations"], -9)
assert_equal(conflicted["walletconflicts"][0], conflicting["txid"])
# Node0 wallet is shutdown
self.stop_node(0)
self.start_node(0)
# The block chain re-orgs and the tx is included in a different block
self.nodes[1].generate(9)
self.nodes[1].sendrawtransaction(tx["hex"])
self.nodes[1].generate(1)
self.nodes[1].sendrawtransaction(conflicted["hex"])
self.nodes[1].generate(1)
# Node0 wallet file is loaded on longest sync'ed node1
self.stop_node(1)
self.nodes[0].backupwallet(os.path.join(self.nodes[0].datadir, 'wallet.bak'))
shutil.copyfile(os.path.join(self.nodes[0].datadir, 'wallet.bak'), os.path.join(self.nodes[1].datadir, 'regtest', 'wallet.dat'))
self.start_node(1)
tx_after_reorg = self.nodes[1].gettransaction(txid)
# Check that normal confirmed tx is confirmed again but with different blockhash
assert_equal(tx_after_reorg["confirmations"], 2)
assert(tx_before_reorg["blockhash"] != tx_after_reorg["blockhash"])
conflicted_after_reorg = self.nodes[1].gettransaction(conflicted_txid)
# Check that conflicted tx is confirmed again with blockhash different than previously conflicting tx
assert_equal(conflicted_after_reorg["confirmations"], 1)
assert(conflicting["blockhash"] != conflicted_after_reorg["blockhash"])
if __name__ == '__main__':
ReorgsRestoreTest().main()