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 ACK442a87cc0a
ryanofsky: utACK442a87cc0a
. 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:
commit
5e202382a9
8 changed files with 261 additions and 87 deletions
|
@ -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 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.depth_in_main_chain = wtx.GetDepthInMainChain(locked_chain);
|
||||
result.time_received = wtx.nTimeReceived;
|
||||
|
|
|
@ -384,8 +384,7 @@ UniValue importprunedfunds(const JSONRPCRequest& request)
|
|||
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Something wrong with merkleblock");
|
||||
}
|
||||
|
||||
wtx.nIndex = txnIndex;
|
||||
wtx.hashBlock = merkleBlock.header.GetHash();
|
||||
wtx.SetConf(CWalletTx::Status::CONFIRMED, merkleBlock.header.GetHash(), txnIndex);
|
||||
|
||||
auto locked_chain = pwallet->chain().lock();
|
||||
LOCK(pwallet->cs_wallet);
|
||||
|
|
|
@ -134,10 +134,10 @@ static void WalletTxToJSON(interfaces::Chain& chain, interfaces::Chain::Lock& lo
|
|||
entry.pushKV("generated", true);
|
||||
if (confirms > 0)
|
||||
{
|
||||
entry.pushKV("blockhash", wtx.hashBlock.GetHex());
|
||||
entry.pushKV("blockindex", wtx.nIndex);
|
||||
entry.pushKV("blockhash", wtx.m_confirm.hashBlock.GetHex());
|
||||
entry.pushKV("blockindex", wtx.m_confirm.nIndex);
|
||||
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);
|
||||
entry.pushKV("blocktime", block_time);
|
||||
} else {
|
||||
|
|
|
@ -249,8 +249,7 @@ BOOST_FIXTURE_TEST_CASE(coin_mark_dirty_immature_credit, TestChain100Setup)
|
|||
LockAssertion lock(::cs_main);
|
||||
LOCK(wallet.cs_wallet);
|
||||
|
||||
wtx.hashBlock = ::ChainActive().Tip()->GetBlockHash();
|
||||
wtx.nIndex = 0;
|
||||
wtx.SetConf(CWalletTx::Status::CONFIRMED, ::ChainActive().Tip()->GetBlockHash(), 0);
|
||||
|
||||
// Call GetImmatureCredit() once before adding the key to the wallet to
|
||||
// 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));
|
||||
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);
|
||||
}
|
||||
LOCK(wallet.cs_wallet);
|
||||
if (block) {
|
||||
wtx.SetConf(CWalletTx::Status::CONFIRMED, block->GetBlockHash(), 0);
|
||||
}
|
||||
wallet.AddToWallet(wtx);
|
||||
return wallet.mapWallet.at(wtx.GetHash()).nTimeSmart;
|
||||
}
|
||||
|
||||
|
@ -382,7 +386,7 @@ public:
|
|||
LOCK(wallet->cs_wallet);
|
||||
auto it = wallet->mapWallet.find(tx->GetHash());
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1110,22 +1110,14 @@ bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose)
|
|||
bool fUpdated = false;
|
||||
if (!fInsertedNew)
|
||||
{
|
||||
// Merge
|
||||
if (!wtxIn.hashUnset() && wtxIn.hashBlock != wtx.hashBlock)
|
||||
{
|
||||
wtx.hashBlock = wtxIn.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;
|
||||
if (wtxIn.m_confirm.status != wtx.m_confirm.status) {
|
||||
wtx.m_confirm.status = wtxIn.m_confirm.status;
|
||||
wtx.m_confirm.nIndex = wtxIn.m_confirm.nIndex;
|
||||
wtx.m_confirm.hashBlock = wtxIn.m_confirm.hashBlock;
|
||||
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)
|
||||
{
|
||||
|
@ -1172,8 +1164,19 @@ bool CWallet::AddToWallet(const CWalletTx& wtxIn, bool fFlushOnClose)
|
|||
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();
|
||||
const auto& ins = mapWallet.emplace(hash, wtxIn);
|
||||
CWalletTx& wtx = ins.first->second;
|
||||
|
@ -1186,14 +1189,14 @@ void CWallet::LoadToWallet(const CWalletTx& wtxIn)
|
|||
auto it = mapWallet.find(txin.prevout.hash);
|
||||
if (it != mapWallet.end()) {
|
||||
CWalletTx& prevtx = it->second;
|
||||
if (prevtx.nIndex == -1 && !prevtx.hashUnset()) {
|
||||
MarkConflicted(prevtx.hashBlock, wtx.GetHash());
|
||||
if (prevtx.isConflicted()) {
|
||||
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;
|
||||
{
|
||||
|
@ -1240,9 +1243,9 @@ bool CWallet::AddToWalletIfInvolvingMe(const CTransactionRef& ptx, const uint256
|
|||
|
||||
CWalletTx wtx(this, ptx);
|
||||
|
||||
// Get merkle branch if transaction was found in a block
|
||||
if (!block_hash.IsNull())
|
||||
wtx.SetMerkleBranch(block_hash, posInBlock);
|
||||
// Block disconnection override an abandoned tx as unconfirmed
|
||||
// which means user may have to call abandontransaction again
|
||||
wtx.SetConf(status, block_hash, posInBlock);
|
||||
|
||||
return AddToWallet(wtx, false);
|
||||
}
|
||||
|
@ -1302,7 +1305,7 @@ bool CWallet::AbandonTransaction(interfaces::Chain::Lock& locked_chain, const ui
|
|||
if (currentconfirm == 0 && !wtx.isAbandoned()) {
|
||||
// If the orig tx was not in block/mempool, none of its spends can be in mempool
|
||||
assert(!wtx.InMempool());
|
||||
wtx.nIndex = -1;
|
||||
wtx.m_confirm.nIndex = 0;
|
||||
wtx.setAbandoned();
|
||||
wtx.MarkDirty();
|
||||
batch.WriteTx(wtx);
|
||||
|
@ -1356,8 +1359,9 @@ void CWallet::MarkConflicted(const uint256& hashBlock, const uint256& hashTx)
|
|||
if (conflictconfirms < currentconfirm) {
|
||||
// Block is 'more conflicted' than current confirm; update.
|
||||
// Mark transaction as conflicted with this block.
|
||||
wtx.nIndex = -1;
|
||||
wtx.hashBlock = hashBlock;
|
||||
wtx.m_confirm.nIndex = 0;
|
||||
wtx.m_confirm.hashBlock = hashBlock;
|
||||
wtx.setConflicted();
|
||||
wtx.MarkDirty();
|
||||
batch.WriteTx(wtx);
|
||||
// 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) {
|
||||
if (!AddToWalletIfInvolvingMe(ptx, block_hash, posInBlock, update_tx))
|
||||
void CWallet::SyncTransaction(const CTransactionRef& ptx, CWalletTx::Status status, const uint256& block_hash, int posInBlock, bool update_tx)
|
||||
{
|
||||
if (!AddToWalletIfInvolvingMe(ptx, status, block_hash, posInBlock, update_tx))
|
||||
return; // Not one of ours
|
||||
|
||||
// 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) {
|
||||
auto locked_chain = chain().lock();
|
||||
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());
|
||||
if (it != mapWallet.end()) {
|
||||
|
@ -1408,22 +1413,14 @@ void CWallet::BlockConnected(const CBlock& block, const std::vector<CTransaction
|
|||
const uint256& block_hash = block.GetHash();
|
||||
auto locked_chain = chain().lock();
|
||||
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++) {
|
||||
SyncTransaction(block.vtx[i], block_hash, i);
|
||||
SyncTransaction(block.vtx[i], CWalletTx::Status::CONFIRMED, block_hash, i);
|
||||
TransactionRemovedFromMempool(block.vtx[i]);
|
||||
}
|
||||
for (const CTransactionRef& ptx : vtxConflicted) {
|
||||
TransactionRemovedFromMempool(ptx);
|
||||
}
|
||||
|
||||
m_last_block_processed = block_hash;
|
||||
}
|
||||
|
@ -1432,8 +1429,12 @@ void CWallet::BlockDisconnected(const CBlock& block) {
|
|||
auto locked_chain = chain().lock();
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
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
|
||||
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)
|
||||
{
|
||||
// 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);
|
||||
|
||||
fFirstRunRet = false;
|
||||
|
@ -4042,7 +4048,7 @@ void CWallet::GetKeyBirthTimes(interfaces::Chain::Lock& locked_chain, std::map<C
|
|||
for (const auto& entry : mapWallet) {
|
||||
// iterate over all wallet transactions...
|
||||
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
|
||||
for (const CTxOut &txout : wtx.tx->vout) {
|
||||
// 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 nTimeSmart = wtx.nTimeReceived;
|
||||
if (!wtx.hashUnset()) {
|
||||
if (!wtx.isUnconfirmed() && !wtx.isAbandoned()) {
|
||||
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 latestEntry = 0;
|
||||
|
||||
|
@ -4115,7 +4121,7 @@ unsigned int CWallet::ComputeTimeSmart(const CWalletTx& wtx) const
|
|||
|
||||
nTimeSmart = std::max(latestEntry, std::min(blocktime, latestNow));
|
||||
} 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;
|
||||
|
@ -4233,6 +4239,11 @@ bool CWallet::Verify(interfaces::Chain& chain, const WalletLocation& location, b
|
|||
// Recover readable keypairs:
|
||||
CWallet dummyWallet(&chain, WalletLocation(), WalletDatabase::CreateDummy());
|
||||
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)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -4627,21 +4638,23 @@ CKeyPool::CKeyPool(const CPubKey& vchPubKeyIn, bool internalIn)
|
|||
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
|
||||
hashBlock = block_hash;
|
||||
m_confirm.hashBlock = block_hash;
|
||||
|
||||
// set the position of the transaction in the block
|
||||
nIndex = posInBlock;
|
||||
m_confirm.nIndex = posInBlock;
|
||||
}
|
||||
|
||||
int CWalletTx::GetDepthInMainChain(interfaces::Chain::Lock& locked_chain) const
|
||||
{
|
||||
if (hashUnset())
|
||||
return 0;
|
||||
if (isUnconfirmed() || isAbandoned()) 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
|
||||
|
|
|
@ -396,7 +396,9 @@ class CWalletTx
|
|||
private:
|
||||
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;
|
||||
|
||||
public:
|
||||
|
@ -457,9 +459,7 @@ public:
|
|||
mutable CAmount nChangeCached;
|
||||
|
||||
CWalletTx(const CWallet* pwalletIn, CTransactionRef arg)
|
||||
: tx(std::move(arg)),
|
||||
hashBlock(uint256()),
|
||||
nIndex(-1)
|
||||
: tx(std::move(arg))
|
||||
{
|
||||
Init(pwalletIn);
|
||||
}
|
||||
|
@ -477,16 +477,37 @@ public:
|
|||
fInMempool = false;
|
||||
nChangeCached = 0;
|
||||
nOrderPos = -1;
|
||||
m_confirm = Confirmation{};
|
||||
}
|
||||
|
||||
CTransactionRef tx;
|
||||
uint256 hashBlock;
|
||||
/* An nIndex == -1 means that hashBlock (in nonzero) refers to the earliest
|
||||
* block in the chain we know this or any in-wallet dependency conflicts
|
||||
* with. Older clients interpret nIndex == -1 as unconfirmed for backward
|
||||
* compatibility.
|
||||
|
||||
/* New transactions start as UNCONFIRMED. At BlockConnected,
|
||||
* they will transition to CONFIRMED. In case of reorg, at BlockDisconnected,
|
||||
* they roll back to UNCONFIRMED. If we detect a conflicting transaction at
|
||||
* 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>
|
||||
void Serialize(Stream& s) const
|
||||
|
@ -502,7 +523,9 @@ public:
|
|||
std::vector<char> dummy_vector1; //!< Used to be vMerkleBranch
|
||||
std::vector<char> dummy_vector2; //!< Used to be vtxPrev
|
||||
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>
|
||||
|
@ -513,7 +536,25 @@ public:
|
|||
std::vector<uint256> dummy_vector1; //!< Used to be vMerkleBranch
|
||||
std::vector<CMerkleTx> dummy_vector2; //!< Used to be vtxPrev
|
||||
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);
|
||||
nTimeSmart = mapValue.count("timesmart") ? (unsigned int)atoi64(mapValue["timesmart"]) : 0;
|
||||
|
@ -590,7 +631,7 @@ public:
|
|||
// in place.
|
||||
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:
|
||||
|
@ -607,10 +648,18 @@ public:
|
|||
* >0 : is a coinbase transaction which matures in this many blocks
|
||||
*/
|
||||
int GetBlocksToMaturity(interfaces::Chain::Lock& locked_chain) const;
|
||||
bool hashUnset() const { return (hashBlock.IsNull() || hashBlock == ABANDON_HASH); }
|
||||
bool isAbandoned() const { return (hashBlock == ABANDON_HASH); }
|
||||
void setAbandoned() { hashBlock = ABANDON_HASH; }
|
||||
|
||||
bool isAbandoned() const { return m_confirm.status == CWalletTx::ABANDONED; }
|
||||
void setAbandoned()
|
||||
{
|
||||
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(); }
|
||||
bool IsCoinBase() const { return tx->IsCoinBase(); }
|
||||
bool IsImmatureCoinBase(interfaces::Chain::Lock& locked_chain) const;
|
||||
|
@ -750,7 +799,7 @@ private:
|
|||
* Abandoned state should probably be more carefully tracked via different
|
||||
* 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. */
|
||||
void MarkConflicted(const uint256& hashBlock, const uint256& hashTx);
|
||||
|
@ -762,7 +811,7 @@ private:
|
|||
|
||||
/* 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. */
|
||||
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) */
|
||||
CHDChain hdChain;
|
||||
|
@ -897,6 +946,9 @@ public:
|
|||
bool IsLocked() const;
|
||||
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);
|
||||
|
||||
typedef std::multimap<int64_t, CWalletTx*> TxItems;
|
||||
|
@ -1042,7 +1094,7 @@ public:
|
|||
|
||||
void MarkDirty();
|
||||
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 BlockConnected(const CBlock& block, const std::vector<CTransactionRef>& vtxConflicted) override;
|
||||
void BlockDisconnected(const CBlock& block) override;
|
||||
|
|
|
@ -131,6 +131,7 @@ BASE_SCRIPTS = [
|
|||
'wallet_createwallet.py --usecli',
|
||||
'wallet_watchonly.py',
|
||||
'wallet_watchonly.py --usecli',
|
||||
'wallet_reorgsrestore.py',
|
||||
'interface_http.py',
|
||||
'interface_rpc.py',
|
||||
'rpc_psbt.py',
|
||||
|
|
105
test/functional/wallet_reorgsrestore.py
Executable file
105
test/functional/wallet_reorgsrestore.py
Executable 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()
|
Loading…
Reference in a new issue