Changeset View
Changeset View
Standalone View
Standalone View
src/wallet/wallet.cpp
Show First 20 Lines • Show All 1,035 Lines • ▼ Show 20 Lines | for (const CTxIn &txin : wtx.tx->vin) { | ||||
if (prevtx.nIndex == -1 && !prevtx.hashUnset()) { | if (prevtx.nIndex == -1 && !prevtx.hashUnset()) { | ||||
MarkConflicted(prevtx.hashBlock, wtx.GetId()); | MarkConflicted(prevtx.hashBlock, wtx.GetId()); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
} | } | ||||
bool CWallet::AddToWalletIfInvolvingMe(const CTransactionRef &ptx, | bool CWallet::AddToWalletIfInvolvingMe(const CTransactionRef &ptx, | ||||
const CBlockIndex *pIndex, | const BlockHash &block_hash, | ||||
int posInBlock, bool fUpdate) { | int posInBlock, bool fUpdate) { | ||||
const CTransaction &tx = *ptx; | const CTransaction &tx = *ptx; | ||||
AssertLockHeld(cs_wallet); | AssertLockHeld(cs_wallet); | ||||
if (pIndex != nullptr) { | if (!block_hash.IsNull()) { | ||||
for (const CTxIn &txin : tx.vin) { | for (const CTxIn &txin : tx.vin) { | ||||
std::pair<TxSpends::const_iterator, TxSpends::const_iterator> | std::pair<TxSpends::const_iterator, TxSpends::const_iterator> | ||||
range = mapTxSpends.equal_range(txin.prevout); | range = mapTxSpends.equal_range(txin.prevout); | ||||
while (range.first != range.second) { | while (range.first != range.second) { | ||||
if (range.first->second != tx.GetId()) { | if (range.first->second != tx.GetId()) { | ||||
WalletLogPrintf( | WalletLogPrintf( | ||||
"Transaction %s (in block %s) conflicts with wallet " | "Transaction %s (in block %s) conflicts with wallet " | ||||
"transaction %s (both spend %s:%i)\n", | "transaction %s (both spend %s:%i)\n", | ||||
tx.GetId().ToString(), | tx.GetId().ToString(), block_hash.ToString(), | ||||
pIndex->GetBlockHash().ToString(), | |||||
range.first->second.ToString(), | range.first->second.ToString(), | ||||
range.first->first.GetTxId().ToString(), | range.first->first.GetTxId().ToString(), | ||||
range.first->first.GetN()); | range.first->first.GetN()); | ||||
MarkConflicted(pIndex->GetBlockHash(), range.first->second); | MarkConflicted(block_hash, range.first->second); | ||||
} | } | ||||
range.first++; | range.first++; | ||||
} | } | ||||
} | } | ||||
} | } | ||||
bool fExisted = mapWallet.count(tx.GetId()) != 0; | bool fExisted = mapWallet.count(tx.GetId()) != 0; | ||||
if (fExisted && !fUpdate) { | if (fExisted && !fUpdate) { | ||||
Show All 30 Lines | if (fExisted || IsMine(tx) || IsFromMe(tx)) { | ||||
} | } | ||||
} | } | ||||
} | } | ||||
} | } | ||||
CWalletTx wtx(this, ptx); | CWalletTx wtx(this, ptx); | ||||
// Get merkle branch if transaction was found in a block | // Get merkle branch if transaction was found in a block | ||||
if (pIndex != nullptr) { | if (!block_hash.IsNull()) { | ||||
wtx.SetMerkleBranch(pIndex, posInBlock); | wtx.SetMerkleBranch(block_hash, posInBlock); | ||||
} | } | ||||
return AddToWallet(wtx, false); | return AddToWallet(wtx, false); | ||||
} | } | ||||
return false; | return false; | ||||
} | } | ||||
▲ Show 20 Lines • Show All 76 Lines • ▼ Show 20 Lines | bool CWallet::AbandonTransaction(interfaces::Chain::Lock &locked_chain, | ||||
return true; | return true; | ||||
} | } | ||||
void CWallet::MarkConflicted(const BlockHash &hashBlock, const TxId &txid) { | void CWallet::MarkConflicted(const BlockHash &hashBlock, const TxId &txid) { | ||||
auto locked_chain = chain().lock(); | auto locked_chain = chain().lock(); | ||||
LOCK(cs_wallet); | LOCK(cs_wallet); | ||||
int conflictconfirms = 0; | int conflictconfirms = -locked_chain->getBlockDepth(hashBlock); | ||||
CBlockIndex *pindex = LookupBlockIndex(hashBlock); | |||||
if (pindex && ::ChainActive().Contains(pindex)) { | |||||
conflictconfirms = -(::ChainActive().Height() - pindex->nHeight + 1); | |||||
} | |||||
// If number of conflict confirms cannot be determined, this means that the | // If number of conflict confirms cannot be determined, this means that the | ||||
// block is still unknown or not yet part of the main chain, for example | // block is still unknown or not yet part of the main chain, for example | ||||
// when loading the wallet during a reindex. Do nothing in that case. | // when loading the wallet during a reindex. Do nothing in that case. | ||||
if (conflictconfirms >= 0) { | if (conflictconfirms >= 0) { | ||||
return; | return; | ||||
} | } | ||||
Show All 34 Lines | while (!todo.empty()) { | ||||
// balance available of the outputs it spends. So force those to be | // balance available of the outputs it spends. So force those to be | ||||
// recomputed. | // recomputed. | ||||
MarkInputsDirty(wtx.tx); | MarkInputsDirty(wtx.tx); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
void CWallet::SyncTransaction(const CTransactionRef &ptx, | void CWallet::SyncTransaction(const CTransactionRef &ptx, | ||||
const CBlockIndex *pindex, int posInBlock, | const BlockHash &block_hash, int posInBlock, | ||||
bool update_tx) { | bool update_tx) { | ||||
if (!AddToWalletIfInvolvingMe(ptx, pindex, posInBlock, update_tx)) { | if (!AddToWalletIfInvolvingMe(ptx, block_hash, posInBlock, update_tx)) { | ||||
// Not one of ours | // Not one of ours | ||||
return; | return; | ||||
} | } | ||||
// If a transaction changes 'conflicted' state, that changes the balance | // If a transaction changes 'conflicted' state, that changes the balance | ||||
// available of the outputs it spends. So force those to be | // available of the outputs it spends. So force those to be | ||||
// recomputed, also: | // recomputed, also: | ||||
MarkInputsDirty(ptx); | MarkInputsDirty(ptx); | ||||
} | } | ||||
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); | SyncTransaction(ptx, BlockHash(), 0 /* position in block */); | ||||
auto it = mapWallet.find(ptx->GetId()); | auto it = mapWallet.find(ptx->GetId()); | ||||
if (it != mapWallet.end()) { | if (it != mapWallet.end()) { | ||||
it->second.fInMempool = true; | it->second.fInMempool = true; | ||||
} | } | ||||
} | } | ||||
void CWallet::TransactionRemovedFromMempool(const CTransactionRef &ptx) { | void CWallet::TransactionRemovedFromMempool(const CTransactionRef &ptx) { | ||||
Show All 13 Lines | void CWallet::BlockConnected( | ||||
// TODO: Temporarily ensure that mempool removals are notified before | // TODO: Temporarily ensure that mempool removals are notified before | ||||
// connected transactions. This shouldn't matter, but the abandoned state of | // connected transactions. This shouldn't matter, but the abandoned state of | ||||
// transactions in our wallet is currently cleared when we receive another | // transactions in our wallet is currently cleared when we receive another | ||||
// notification and there is a race condition where notification of a | // notification and there is a race condition where notification of a | ||||
// connected conflict might cause an outside process to abandon a | // connected conflict might cause an outside process to abandon a | ||||
// transaction and then have it inadvertently cleared by the notification | // transaction and then have it inadvertently cleared by the notification | ||||
// that the conflicted transaction was evicted. | // that the conflicted transaction was evicted. | ||||
for (const CTransactionRef &ptx : vtxConflicted) { | for (const CTransactionRef &ptx : vtxConflicted) { | ||||
SyncTransaction(ptx); | SyncTransaction(ptx, BlockHash(), 0 /* position in block */); | ||||
TransactionRemovedFromMempool(ptx); | TransactionRemovedFromMempool(ptx); | ||||
} | } | ||||
for (size_t i = 0; i < pblock->vtx.size(); i++) { | for (size_t i = 0; i < pblock->vtx.size(); i++) { | ||||
SyncTransaction(pblock->vtx[i], pindex, i); | SyncTransaction(pblock->vtx[i], pindex->GetBlockHash(), i); | ||||
TransactionRemovedFromMempool(pblock->vtx[i]); | TransactionRemovedFromMempool(pblock->vtx[i]); | ||||
} | } | ||||
m_last_block_processed = pindex; | m_last_block_processed = pindex->GetBlockHash(); | ||||
} | } | ||||
void CWallet::BlockDisconnected(const std::shared_ptr<const CBlock> &pblock) { | void CWallet::BlockDisconnected(const std::shared_ptr<const CBlock> &pblock) { | ||||
auto locked_chain = chain().lock(); | auto locked_chain = chain().lock(); | ||||
LOCK(cs_wallet); | LOCK(cs_wallet); | ||||
for (const CTransactionRef &ptx : pblock->vtx) { | for (const CTransactionRef &ptx : pblock->vtx) { | ||||
SyncTransaction(ptx); | SyncTransaction(ptx, BlockHash(), 0 /* position in block */); | ||||
} | } | ||||
} | } | ||||
void CWallet::BlockUntilSyncedToCurrentChain() { | void CWallet::BlockUntilSyncedToCurrentChain() { | ||||
AssertLockNotHeld(cs_main); | AssertLockNotHeld(cs_main); | ||||
AssertLockNotHeld(cs_wallet); | AssertLockNotHeld(cs_wallet); | ||||
{ | { | ||||
// Skip the queue-draining stuff if we know we're caught up with | // Skip the queue-draining stuff if we know we're caught up with | ||||
// ::ChainActive().Tip()... | // ::ChainActive().Tip()... | ||||
// We could also take cs_wallet here, and call m_last_block_processed | // We could also take cs_wallet here, and call m_last_block_processed | ||||
// protected by cs_wallet instead of cs_main, but as long as we need | // protected by cs_wallet instead of cs_main, but as long as we need | ||||
// cs_main here anyway, it's easier to just call it cs_main-protected. | // cs_main here anyway, it's easier to just call it cs_main-protected. | ||||
auto locked_chain = chain().lock(); | auto locked_chain = chain().lock(); | ||||
const CBlockIndex *initialChainTip = ::ChainActive().Tip(); | if (!m_last_block_processed.IsNull() && | ||||
if (m_last_block_processed && | locked_chain->isPotentialTip(m_last_block_processed)) { | ||||
m_last_block_processed->GetAncestor(initialChainTip->nHeight) == | |||||
initialChainTip) { | |||||
return; | return; | ||||
} | } | ||||
} | } | ||||
// ...otherwise put a callback in the validation interface queue and wait | // ...otherwise put a callback in the validation interface queue and wait | ||||
// for the queue to drain enough to execute it (indicating we are caught up | // for the queue to drain enough to execute it (indicating we are caught up | ||||
// at least with the time we entered this function). | // at least with the time we entered this function). | ||||
SyncWithValidationInterfaceQueue(); | SyncWithValidationInterfaceQueue(); | ||||
▲ Show 20 Lines • Show All 436 Lines • ▼ Show 20 Lines | |||||
* returned will be higher than startTime if relevant blocks could not be read. | * returned will be higher than startTime if relevant blocks could not be read. | ||||
*/ | */ | ||||
int64_t CWallet::RescanFromTime(int64_t startTime, | int64_t CWallet::RescanFromTime(int64_t startTime, | ||||
const WalletRescanReserver &reserver, | const WalletRescanReserver &reserver, | ||||
bool update) { | bool update) { | ||||
// Find starting block. May be null if nCreateTime is greater than the | // Find starting block. May be null if nCreateTime is greater than the | ||||
// highest blockchain timestamp, in which case there is nothing that needs | // highest blockchain timestamp, in which case there is nothing that needs | ||||
// to be scanned. | // to be scanned. | ||||
CBlockIndex *startBlock = nullptr; | BlockHash start_block; | ||||
{ | { | ||||
auto locked_chain = chain().lock(); | auto locked_chain = chain().lock(); | ||||
startBlock = | const Optional<int> start_height = locked_chain->findFirstBlockWithTime( | ||||
::ChainActive().FindEarliestAtLeast(startTime - TIMESTAMP_WINDOW); | startTime - TIMESTAMP_WINDOW, &start_block); | ||||
WalletLogPrintf("%s: Rescanning last %i blocks\n", __func__, | const Optional<int> tip_height = locked_chain->getHeight(); | ||||
startBlock | WalletLogPrintf( | ||||
? ::ChainActive().Height() - startBlock->nHeight + 1 | "%s: Rescanning last %i blocks\n", __func__, | ||||
: 0); | tip_height && start_height ? *tip_height - *start_height + 1 : 0); | ||||
} | } | ||||
if (startBlock) { | if (!start_block.IsNull()) { | ||||
const CBlockIndex *failedBlock, *stop_block; | |||||
// TODO: this should take into account failure by ScanResult::USER_ABORT | // TODO: this should take into account failure by ScanResult::USER_ABORT | ||||
if (ScanResult::FAILURE == | ScanResult result = ScanForWalletTransactions(start_block, BlockHash(), | ||||
ScanForWalletTransactions(startBlock, nullptr, reserver, | reserver, update); | ||||
failedBlock, stop_block, update)) { | if (result.status == ScanResult::FAILURE) { | ||||
return failedBlock->GetBlockTimeMax() + TIMESTAMP_WINDOW + 1; | int64_t time_max; | ||||
if (!chain().findBlock(result.failed_block, nullptr /* block */, | |||||
nullptr /* time */, &time_max)) { | |||||
throw std::logic_error( | |||||
"ScanForWalletTransactions returned invalid block hash"); | |||||
} | |||||
return time_max + TIMESTAMP_WINDOW + 1; | |||||
} | } | ||||
} | } | ||||
return startTime; | return startTime; | ||||
} | } | ||||
/** | /** | ||||
* Scan the block chain (starting in pindexStart) for transactions from or to | * Scan the block chain (starting in start_block) for transactions from or to | ||||
* us. If fUpdate is true, found transactions that already exist in the wallet | * us. If fUpdate is true, found transactions that already exist in the wallet | ||||
* will be updated. | * will be updated. | ||||
* | * | ||||
* @param[in] pindexStop if not a nullptr, the scan will stop at this | * @param[in] start_block if not null, the scan will start at this block instead | ||||
* block-index | * of the genesis block | ||||
* @param[out] failed_block if FAILURE is returned, the most recent block | * @param[in] stop_block if not null, the scan will stop at this block instead | ||||
* that could not be scanned, otherwise nullptr | * of the chain tip | ||||
* @param[out] stop_block the most recent block that could be scanned, | |||||
* otherwise nullptr if no block could be scanned | |||||
* | * | ||||
* @return ScanResult indicating success or failure of the scan. SUCCESS if | * @return ScanResult indicating success or failure of the scan. SUCCESS if | ||||
* scan was successful. FAILURE if a complete rescan was not possible (due to | * scan was successful. FAILURE if a complete rescan was not possible (due to | ||||
* pruning or corruption). USER_ABORT if the rescan was aborted before it | * pruning or corruption). USER_ABORT if the rescan was aborted before it | ||||
* could complete. | * could complete. | ||||
* | * | ||||
* @pre Caller needs to make sure pindexStop (and the optional pindexStart) are | * @pre Caller needs to make sure start_block (and the optional stop_block) are | ||||
* on the main chain after to the addition of any new keys you want to detect | * on the main chain after to the addition of any new keys you want to detect | ||||
* transactions for. | * transactions for. | ||||
*/ | */ | ||||
CWallet::ScanResult CWallet::ScanForWalletTransactions( | CWallet::ScanResult CWallet::ScanForWalletTransactions( | ||||
const CBlockIndex *const pindexStart, const CBlockIndex *const pindexStop, | const BlockHash &start_block, const BlockHash &stop_block, | ||||
const WalletRescanReserver &reserver, const CBlockIndex *&failed_block, | const WalletRescanReserver &reserver, bool fUpdate) { | ||||
const CBlockIndex *&stop_block, bool fUpdate) { | |||||
int64_t nNow = GetTime(); | int64_t nNow = GetTime(); | ||||
assert(reserver.isReserved()); | assert(reserver.isReserved()); | ||||
if (pindexStop) { | |||||
assert(pindexStop->nHeight >= pindexStart->nHeight); | |||||
} | |||||
const CBlockIndex *pindex = pindexStart; | BlockHash block_hash = start_block; | ||||
failed_block = nullptr; | ScanResult result; | ||||
stop_block = nullptr; | |||||
if (pindex) { | WalletLogPrintf("Rescan started from block %s...\n", | ||||
WalletLogPrintf("Rescan started from block %d...\n", pindex->nHeight); | start_block.ToString()); | ||||
} | |||||
{ | { | ||||
fAbortRescan = false; | fAbortRescan = false; | ||||
// Show rescan progress in GUI as dialog or on splashscreen, if -rescan | // Show rescan progress in GUI as dialog or on splashscreen, if -rescan | ||||
// on startup. | // on startup. | ||||
ShowProgress(strprintf("%s " + _("Rescanning..."), GetDisplayName()), | ShowProgress(strprintf("%s " + _("Rescanning..."), GetDisplayName()), | ||||
0); | 0); | ||||
CBlockIndex *tip = nullptr; | BlockHash tip_hash; | ||||
Optional<int> block_height; | |||||
double progress_begin; | double progress_begin; | ||||
double progress_end; | double progress_end; | ||||
{ | { | ||||
auto locked_chain = chain().lock(); | auto locked_chain = chain().lock(); | ||||
progress_begin = | if (Optional<int> tip_height = locked_chain->getHeight()) { | ||||
GuessVerificationProgress(chainParams.TxData(), pindex); | tip_hash = locked_chain->getBlockHash(*tip_height); | ||||
if (pindexStop == nullptr) { | |||||
tip = ::ChainActive().Tip(); | |||||
progress_end = | |||||
GuessVerificationProgress(chainParams.TxData(), tip); | |||||
} else { | |||||
progress_end = | |||||
GuessVerificationProgress(chainParams.TxData(), pindexStop); | |||||
} | } | ||||
block_height = locked_chain->getBlockHeight(block_hash); | |||||
progress_begin = chain().guessVerificationProgress(block_hash); | |||||
progress_end = chain().guessVerificationProgress( | |||||
stop_block.IsNull() ? tip_hash : stop_block); | |||||
} | } | ||||
double progress_current = progress_begin; | double progress_current = progress_begin; | ||||
while (pindex && !fAbortRescan && !ShutdownRequested()) { | while (block_height && !fAbortRescan && !ShutdownRequested()) { | ||||
if (pindex->nHeight % 100 == 0 && | if (*block_height % 100 == 0 && | ||||
progress_end - progress_begin > 0.0) { | progress_end - progress_begin > 0.0) { | ||||
ShowProgress( | ShowProgress( | ||||
strprintf("%s " + _("Rescanning..."), GetDisplayName()), | strprintf("%s " + _("Rescanning..."), GetDisplayName()), | ||||
std::max( | std::max( | ||||
1, | 1, | ||||
std::min(99, (int)((progress_current - progress_begin) / | std::min(99, (int)((progress_current - progress_begin) / | ||||
(progress_end - progress_begin) * | (progress_end - progress_begin) * | ||||
100)))); | 100)))); | ||||
} | } | ||||
if (GetTime() >= nNow + 60) { | if (GetTime() >= nNow + 60) { | ||||
nNow = GetTime(); | nNow = GetTime(); | ||||
WalletLogPrintf("Still rescanning. At block %d. Progress=%f\n", | WalletLogPrintf("Still rescanning. At block %d. Progress=%f\n", | ||||
pindex->nHeight, progress_current); | *block_height, progress_current); | ||||
} | } | ||||
CBlock block; | CBlock block; | ||||
if (ReadBlockFromDisk(block, pindex, chainParams.GetConsensus())) { | if (chain().findBlock(block_hash, &block) && !block.IsNull()) { | ||||
auto locked_chain = chain().lock(); | auto locked_chain = chain().lock(); | ||||
LOCK(cs_wallet); | LOCK(cs_wallet); | ||||
if (pindex && !::ChainActive().Contains(pindex)) { | if (!locked_chain->getBlockHeight(block_hash)) { | ||||
// Abort scan if current block is no longer active, to | // Abort scan if current block is no longer active, to | ||||
// prevent marking transactions as coming from the wrong | // prevent marking transactions as coming from the wrong | ||||
// block. | // block. | ||||
failed_block = pindex; | // TODO: This should return success instead of failure, see | ||||
// https://github.com/bitcoin/bitcoin/pull/14711#issuecomment-458342518 | |||||
result.failed_block = block_hash; | |||||
result.status = ScanResult::FAILURE; | |||||
break; | break; | ||||
} | } | ||||
for (size_t posInBlock = 0; posInBlock < block.vtx.size(); | for (size_t posInBlock = 0; posInBlock < block.vtx.size(); | ||||
++posInBlock) { | ++posInBlock) { | ||||
SyncTransaction(block.vtx[posInBlock], pindex, posInBlock, | SyncTransaction(block.vtx[posInBlock], block_hash, | ||||
fUpdate); | posInBlock, fUpdate); | ||||
} | } | ||||
// scan succeeded, record block as most recent successfully | // scan succeeded, record block as most recent successfully | ||||
// scanned | // scanned | ||||
stop_block = pindex; | result.stop_block = block_hash; | ||||
result.stop_height = *block_height; | |||||
} else { | } else { | ||||
// could not scan block, keep scanning but record this block as | // could not scan block, keep scanning but record this block as | ||||
// the most recent failure | // the most recent failure | ||||
failed_block = pindex; | result.failed_block = block_hash; | ||||
result.status = ScanResult::FAILURE; | |||||
} | } | ||||
if (pindex == pindexStop) { | if (block_hash == stop_block) { | ||||
break; | break; | ||||
} | } | ||||
{ | { | ||||
auto locked_chain = chain().lock(); | auto locked_chain = chain().lock(); | ||||
pindex = ::ChainActive().Next(pindex); | Optional<int> tip_height = locked_chain->getHeight(); | ||||
if (!tip_height || *tip_height <= block_height || | |||||
!locked_chain->getBlockHeight(block_hash)) { | |||||
// break successfully when rescan has reached the tip, or | |||||
// previous block is no longer on the chain due to a reorg | |||||
break; | |||||
} | |||||
// increment block and verification progress | |||||
block_hash = locked_chain->getBlockHash(++*block_height); | |||||
progress_current = | progress_current = | ||||
GuessVerificationProgress(chainParams.TxData(), pindex); | chain().guessVerificationProgress(block_hash); | ||||
if (pindexStop == nullptr && tip != ::ChainActive().Tip()) { | |||||
tip = ::ChainActive().Tip(); | // handle updated tip hash | ||||
const BlockHash prev_tip_hash = tip_hash; | |||||
tip_hash = locked_chain->getBlockHash(*tip_height); | |||||
if (stop_block.IsNull() && prev_tip_hash != tip_hash) { | |||||
// in case the tip has changed, update progress max | // in case the tip has changed, update progress max | ||||
progress_end = | progress_end = chain().guessVerificationProgress(tip_hash); | ||||
GuessVerificationProgress(chainParams.TxData(), tip); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
// Hide progress dialog in GUI. | // Hide progress dialog in GUI. | ||||
ShowProgress(strprintf("%s " + _("Rescanning..."), GetDisplayName()), | ShowProgress(strprintf("%s " + _("Rescanning..."), GetDisplayName()), | ||||
100); | 100); | ||||
if (pindex && fAbortRescan) { | if (block_height && fAbortRescan) { | ||||
WalletLogPrintf("Rescan aborted at block %d. Progress=%f\n", | WalletLogPrintf("Rescan aborted at block %d. Progress=%f\n", | ||||
pindex->nHeight, progress_current); | block_height.value_or(0), progress_current); | ||||
return ScanResult::USER_ABORT; | result.status = ScanResult::USER_ABORT; | ||||
} else if (pindex && ShutdownRequested()) { | } else if (block_height && ShutdownRequested()) { | ||||
WalletLogPrintf("Rescan interrupted by shutdown request at block " | WalletLogPrintf("Rescan interrupted by shutdown request at block " | ||||
"%d. Progress=%f\n", | "%d. Progress=%f\n", | ||||
pindex->nHeight, progress_current); | block_height.value_or(0), progress_current); | ||||
return ScanResult::USER_ABORT; | result.status = ScanResult::USER_ABORT; | ||||
} | } | ||||
} | } | ||||
return failed_block ? ScanResult::FAILURE : ScanResult::SUCCESS; | return result; | ||||
} | } | ||||
void CWallet::ReacceptWalletTransactions() { | void CWallet::ReacceptWalletTransactions() { | ||||
// If transactions aren't being broadcasted, don't let them into local | // If transactions aren't being broadcasted, don't let them into local | ||||
// mempool either. | // mempool either. | ||||
if (!fBroadcastTransactions) { | if (!fBroadcastTransactions) { | ||||
return; | return; | ||||
} | } | ||||
▲ Show 20 Lines • Show All 983 Lines • ▼ Show 20 Lines | |||||
} | } | ||||
/** | /** | ||||
* Return a height-based locktime for new transactions (uses the height of the | * Return a height-based locktime for new transactions (uses the height of the | ||||
* current chain tip unless we are not synced with the current chain | * current chain tip unless we are not synced with the current chain | ||||
*/ | */ | ||||
static uint32_t | static uint32_t | ||||
GetLocktimeForNewTransaction(interfaces::Chain::Lock &locked_chain) { | GetLocktimeForNewTransaction(interfaces::Chain::Lock &locked_chain) { | ||||
uint32_t const height = locked_chain.getHeight().value_or(-1); | |||||
uint32_t locktime; | uint32_t locktime; | ||||
// Discourage fee sniping. | // Discourage fee sniping. | ||||
// | // | ||||
// For a large miner the value of the transactions in the best block and | // For a large miner the value of the transactions in the best block and | ||||
// the mempool can exceed the cost of deliberately attempting to mine two | // the mempool can exceed the cost of deliberately attempting to mine two | ||||
// blocks to orphan the current best block. By setting nLockTime such that | // blocks to orphan the current best block. By setting nLockTime such that | ||||
// only the next block can include the transaction, we discourage this | // only the next block can include the transaction, we discourage this | ||||
// practice as the height restricted and limited blocksize gives miners | // practice as the height restricted and limited blocksize gives miners | ||||
// considering fee sniping fewer options for pulling off this attack. | // considering fee sniping fewer options for pulling off this attack. | ||||
// | // | ||||
// A simple way to think about this is from the wallet's point of view we | // A simple way to think about this is from the wallet's point of view we | ||||
// always want the blockchain to move forward. By setting nLockTime this | // always want the blockchain to move forward. By setting nLockTime this | ||||
// way we're basically making the statement that we only want this | // way we're basically making the statement that we only want this | ||||
// transaction to appear in the next block; we don't want to potentially | // transaction to appear in the next block; we don't want to potentially | ||||
// encourage reorgs by allowing transactions to appear at lower heights | // encourage reorgs by allowing transactions to appear at lower heights | ||||
// than the next block in forks of the best chain. | // than the next block in forks of the best chain. | ||||
// | // | ||||
// Of course, the subsidy is high enough, and transaction volume low | // Of course, the subsidy is high enough, and transaction volume low | ||||
// enough, that fee sniping isn't a problem yet, but by implementing a fix | // enough, that fee sniping isn't a problem yet, but by implementing a fix | ||||
// now we ensure code won't be written that makes assumptions about | // now we ensure code won't be written that makes assumptions about | ||||
// nLockTime that preclude a fix later. | // nLockTime that preclude a fix later. | ||||
if (IsCurrentForAntiFeeSniping(locked_chain)) { | if (IsCurrentForAntiFeeSniping(locked_chain)) { | ||||
locktime = ChainActive().Height(); | locktime = height; | ||||
// Secondly occasionally randomly pick a nLockTime even further back, so | // Secondly occasionally randomly pick a nLockTime even further back, so | ||||
// that transactions that are delayed after signing for whatever reason, | // that transactions that are delayed after signing for whatever reason, | ||||
// e.g. high-latency mix networks and some CoinJoin implementations, | // e.g. high-latency mix networks and some CoinJoin implementations, | ||||
// have better privacy. | // have better privacy. | ||||
if (GetRandInt(10) == 0) { | if (GetRandInt(10) == 0) { | ||||
locktime = std::max(0, int(locktime) - GetRandInt(100)); | locktime = std::max(0, int(locktime) - GetRandInt(100)); | ||||
} | } | ||||
} else { | } else { | ||||
// If our chain is lagging behind, we can't discourage fee sniping nor | // If our chain is lagging behind, we can't discourage fee sniping nor | ||||
// help the privacy of high-latency transactions. To avoid leaking a | // help the privacy of high-latency transactions. To avoid leaking a | ||||
// potentially unique "nLockTime fingerprint", set nLockTime to a | // potentially unique "nLockTime fingerprint", set nLockTime to a | ||||
// constant. | // constant. | ||||
locktime = 0; | locktime = 0; | ||||
} | } | ||||
assert(locktime <= uint32_t(ChainActive().Height())); | assert(locktime <= height); | ||||
assert(locktime < LOCKTIME_THRESHOLD); | assert(locktime < LOCKTIME_THRESHOLD); | ||||
return locktime; | return locktime; | ||||
} | } | ||||
OutputType | OutputType | ||||
CWallet::TransactionChangeType(OutputType change_type, | CWallet::TransactionChangeType(OutputType change_type, | ||||
const std::vector<CRecipient> &vecSend) { | const std::vector<CRecipient> &vecSend) { | ||||
// If -changetype is specified, always use that change type. | // If -changetype is specified, always use that change type. | ||||
▲ Show 20 Lines • Show All 1,175 Lines • ▼ Show 20 Lines | void CWallet::GetKeyBirthTimes( | ||||
// Get birth times for keys with metadata. | // Get birth times for keys with metadata. | ||||
for (const auto &entry : mapKeyMetadata) { | for (const auto &entry : mapKeyMetadata) { | ||||
if (entry.second.nCreateTime) { | if (entry.second.nCreateTime) { | ||||
mapKeyBirth[entry.first] = entry.second.nCreateTime; | mapKeyBirth[entry.first] = entry.second.nCreateTime; | ||||
} | } | ||||
} | } | ||||
// Map in which we'll infer heights of other keys the tip can be | // Map in which we'll infer heights of other keys | ||||
// reorganized; use a 144-block safety margin. | const Optional<int> tip_height = locked_chain.getHeight(); | ||||
CBlockIndex *pindexMax = | // the tip can be reorganized; use a 144-block safety margin | ||||
::ChainActive()[std::max(0, ::ChainActive().Height() - 144)]; | const int max_height = | ||||
std::map<CKeyID, CBlockIndex *> mapKeyFirstBlock; | tip_height && *tip_height > 144 ? *tip_height - 144 : 0; | ||||
std::map<CKeyID, int> mapKeyFirstBlock; | |||||
for (const CKeyID &keyid : GetKeys()) { | for (const CKeyID &keyid : GetKeys()) { | ||||
if (mapKeyBirth.count(keyid) == 0) { | if (mapKeyBirth.count(keyid) == 0) { | ||||
mapKeyFirstBlock[keyid] = pindexMax; | mapKeyFirstBlock[keyid] = max_height; | ||||
} | } | ||||
} | } | ||||
// If there are no such keys, we're done. | // If there are no such keys, we're done. | ||||
if (mapKeyFirstBlock.empty()) { | if (mapKeyFirstBlock.empty()) { | ||||
return; | return; | ||||
} | } | ||||
// Find first block that affects those keys, if there are any left. | // Find first block that affects those keys, if there are any left. | ||||
std::vector<CKeyID> vAffected; | std::vector<CKeyID> vAffected; | ||||
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; | ||||
CBlockIndex *pindex = LookupBlockIndex(wtx.hashBlock); | if (Optional<int> height = locked_chain.getBlockHeight(wtx.hashBlock)) { | ||||
if (pindex && ::ChainActive().Contains(pindex)) { | |||||
// ... which are already in a block | // ... which are already in a block | ||||
int nHeight = pindex->nHeight; | |||||
for (const CTxOut &txout : wtx.tx->vout) { | for (const CTxOut &txout : wtx.tx->vout) { | ||||
// Iterate over all their outputs... | // Iterate over all their outputs... | ||||
CAffectedKeysVisitor(*this, vAffected) | CAffectedKeysVisitor(*this, vAffected) | ||||
.Process(txout.scriptPubKey); | .Process(txout.scriptPubKey); | ||||
for (const CKeyID &keyid : vAffected) { | for (const CKeyID &keyid : vAffected) { | ||||
// ... and all their affected keys. | // ... and all their affected keys. | ||||
std::map<CKeyID, CBlockIndex *>::iterator rit = | std::map<CKeyID, int>::iterator rit = | ||||
mapKeyFirstBlock.find(keyid); | mapKeyFirstBlock.find(keyid); | ||||
if (rit != mapKeyFirstBlock.end() && | if (rit != mapKeyFirstBlock.end() && | ||||
nHeight < rit->second->nHeight) { | *height < rit->second) { | ||||
rit->second = pindex; | rit->second = *height; | ||||
} | } | ||||
} | } | ||||
vAffected.clear(); | vAffected.clear(); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
// Extract block timestamps for those keys. | // Extract block timestamps for those keys. | ||||
for (const auto &entry : mapKeyFirstBlock) { | for (const auto &entry : mapKeyFirstBlock) { | ||||
// block times can be 2h off | // block times can be 2h off | ||||
mapKeyBirth[entry.first] = | mapKeyBirth[entry.first] = | ||||
entry.second->GetBlockTime() - TIMESTAMP_WINDOW; | locked_chain.getBlockTime(entry.second) - TIMESTAMP_WINDOW; | ||||
} | } | ||||
} | } | ||||
/** | /** | ||||
* Compute smart timestamp for a transaction being added to the wallet. | * Compute smart timestamp for a transaction being added to the wallet. | ||||
* | * | ||||
* Logic: | * Logic: | ||||
* - If sending a transaction, assign its timestamp to the current time. | * - If sending a transaction, assign its timestamp to the current time. | ||||
Show All 11 Lines | |||||
* | * | ||||
* For more information see CWalletTx::nTimeSmart, | * For more information see CWalletTx::nTimeSmart, | ||||
* https://bitcointalk.org/?topic=54527, or | * https://bitcointalk.org/?topic=54527, or | ||||
* https://github.com/bitcoin/bitcoin/pull/1393. | * https://github.com/bitcoin/bitcoin/pull/1393. | ||||
*/ | */ | ||||
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.hashUnset()) { | ||||
if (const CBlockIndex *pindex = LookupBlockIndex(wtx.hashBlock)) { | int64_t blocktime; | ||||
if (chain().findBlock(wtx.hashBlock, nullptr /* block */, &blocktime)) { | |||||
int64_t latestNow = wtx.nTimeReceived; | int64_t latestNow = wtx.nTimeReceived; | ||||
int64_t latestEntry = 0; | int64_t latestEntry = 0; | ||||
// Tolerate times up to the last timestamp in the wallet not more | // Tolerate times up to the last timestamp in the wallet not more | ||||
// than 5 minutes into the future | // than 5 minutes into the future | ||||
int64_t latestTolerated = latestNow + 300; | int64_t latestTolerated = latestNow + 300; | ||||
const TxItems &txOrdered = wtxOrdered; | const TxItems &txOrdered = wtxOrdered; | ||||
for (auto it = txOrdered.rbegin(); it != txOrdered.rend(); ++it) { | for (auto it = txOrdered.rbegin(); it != txOrdered.rend(); ++it) { | ||||
Show All 10 Lines | if (!wtx.hashUnset()) { | ||||
latestEntry = nSmartTime; | latestEntry = nSmartTime; | ||||
if (nSmartTime > latestNow) { | if (nSmartTime > latestNow) { | ||||
latestNow = nSmartTime; | latestNow = nSmartTime; | ||||
} | } | ||||
break; | break; | ||||
} | } | ||||
} | } | ||||
int64_t blocktime = pindex->GetBlockTime(); | |||||
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__, | WalletLogPrintf("%s: found %s in block %s not in index\n", __func__, | ||||
wtx.GetId().ToString(), wtx.hashBlock.ToString()); | wtx.GetId().ToString(), wtx.hashBlock.ToString()); | ||||
} | } | ||||
} | } | ||||
return nTimeSmart; | return nTimeSmart; | ||||
} | } | ||||
▲ Show 20 Lines • Show All 295 Lines • ▼ Show 20 Lines | if (fFirstRun) { | ||||
if (walletInstance->CanGenerateKeys() && | if (walletInstance->CanGenerateKeys() && | ||||
!walletInstance->TopUpKeyPool()) { | !walletInstance->TopUpKeyPool()) { | ||||
InitError(_("Unable to generate initial keys")); | InitError(_("Unable to generate initial keys")); | ||||
return nullptr; | return nullptr; | ||||
} | } | ||||
// Temporary. Removed in upcoming lock cleanup | // Temporary. Removed in upcoming lock cleanup | ||||
auto locked_chain = chain.assumeLocked(); | auto locked_chain = chain.assumeLocked(); | ||||
walletInstance->ChainStateFlushed(::ChainActive().GetLocator()); | walletInstance->ChainStateFlushed(locked_chain->getLocator()); | ||||
} else if (wallet_creation_flags & WALLET_FLAG_DISABLE_PRIVATE_KEYS) { | } else if (wallet_creation_flags & WALLET_FLAG_DISABLE_PRIVATE_KEYS) { | ||||
// Make it impossible to disable private keys after creation | // Make it impossible to disable private keys after creation | ||||
InitError(strprintf(_("Error loading %s: Private keys can only be " | InitError(strprintf(_("Error loading %s: Private keys can only be " | ||||
"disabled during creation"), | "disabled during creation"), | ||||
walletFile)); | walletFile)); | ||||
return nullptr; | return nullptr; | ||||
} else if (walletInstance->IsWalletFlagSet( | } else if (walletInstance->IsWalletFlagSet( | ||||
WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { | WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { | ||||
▲ Show 20 Lines • Show All 66 Lines • ▼ Show 20 Lines | std::shared_ptr<CWallet> CWallet::CreateWalletFromFile( | ||||
walletInstance->m_default_change_type = DEFAULT_CHANGE_TYPE; | walletInstance->m_default_change_type = DEFAULT_CHANGE_TYPE; | ||||
walletInstance->WalletLogPrintf("Wallet completed loading in %15dms\n", | walletInstance->WalletLogPrintf("Wallet completed loading in %15dms\n", | ||||
GetTimeMillis() - nStart); | GetTimeMillis() - nStart); | ||||
// Try to top up keypool. No-op if the wallet is locked. | // Try to top up keypool. No-op if the wallet is locked. | ||||
walletInstance->TopUpKeyPool(); | walletInstance->TopUpKeyPool(); | ||||
// Temporary, for FindForkInGlobalIndex below. Removed in upcoming commit. | |||||
LockAnnotation lock(::cs_main); | |||||
auto locked_chain = chain.lock(); | auto locked_chain = chain.lock(); | ||||
LOCK(walletInstance->cs_wallet); | LOCK(walletInstance->cs_wallet); | ||||
CBlockIndex *pindexRescan = ::ChainActive().Genesis(); | int rescan_height = 0; | ||||
if (!gArgs.GetBoolArg("-rescan", false)) { | if (!gArgs.GetBoolArg("-rescan", false)) { | ||||
WalletBatch batch(*walletInstance->database); | WalletBatch batch(*walletInstance->database); | ||||
CBlockLocator locator; | CBlockLocator locator; | ||||
if (batch.ReadBestBlock(locator)) { | if (batch.ReadBestBlock(locator)) { | ||||
pindexRescan = FindForkInGlobalIndex(::ChainActive(), locator); | if (const Optional<int> fork_height = | ||||
locked_chain->findLocatorFork(locator)) { | |||||
rescan_height = *fork_height; | |||||
} | |||||
} | } | ||||
} | } | ||||
walletInstance->m_last_block_processed = ::ChainActive().Tip(); | const Optional<int> tip_height = locked_chain->getHeight(); | ||||
if (tip_height) { | |||||
walletInstance->m_last_block_processed = | |||||
locked_chain->getBlockHash(*tip_height); | |||||
} else { | |||||
walletInstance->m_last_block_processed.SetNull(); | |||||
} | |||||
if (::ChainActive().Tip() && ::ChainActive().Tip() != pindexRescan) { | if (tip_height && *tip_height != rescan_height) { | ||||
// We can't rescan beyond non-pruned blocks, stop and throw an error. | // We can't rescan beyond non-pruned blocks, stop and throw an error. | ||||
// This might happen if a user uses an old wallet within a pruned node | // This might happen if a user uses an old wallet within a pruned node | ||||
// or if he ran -disablewallet for a longer time, then decided to | // or if he ran -disablewallet for a longer time, then decided to | ||||
// re-enable. | // re-enable. | ||||
if (fPruneMode) { | if (fPruneMode) { | ||||
CBlockIndex *block = ::ChainActive().Tip(); | int block_height = *tip_height; | ||||
while (block && block->pprev && block->pprev->nStatus.hasData() && | while (block_height > 0 && | ||||
block->pprev->nTx > 0 && pindexRescan != block) { | locked_chain->haveBlockOnDisk(block_height - 1) && | ||||
block = block->pprev; | rescan_height != block_height) { | ||||
--block_height; | |||||
} | } | ||||
if (pindexRescan != block) { | if (rescan_height != block_height) { | ||||
InitError(_("Prune: last wallet synchronisation goes beyond " | InitError(_("Prune: last wallet synchronisation goes beyond " | ||||
"pruned data. You need to -reindex (download the " | "pruned data. You need to -reindex (download the " | ||||
"whole blockchain again in case of pruned node)")); | "whole blockchain again in case of pruned node)")); | ||||
return nullptr; | return nullptr; | ||||
} | } | ||||
} | } | ||||
uiInterface.InitMessage(_("Rescanning...")); | uiInterface.InitMessage(_("Rescanning...")); | ||||
walletInstance->WalletLogPrintf( | walletInstance->WalletLogPrintf( | ||||
"Rescanning last %i blocks (from block %i)...\n", | "Rescanning last %i blocks (from block %i)...\n", | ||||
::ChainActive().Height() - pindexRescan->nHeight, | *tip_height - rescan_height, rescan_height); | ||||
pindexRescan->nHeight); | |||||
// No need to read and scan block if block was created before our wallet | // No need to read and scan block if block was created before our wallet | ||||
// birthday (as adjusted for block time variability) | // birthday (as adjusted for block time variability) | ||||
while (pindexRescan && walletInstance->nTimeFirstKey && | if (walletInstance->nTimeFirstKey) { | ||||
(pindexRescan->GetBlockTime() < | if (Optional<int> first_block = | ||||
(walletInstance->nTimeFirstKey - TIMESTAMP_WINDOW))) { | locked_chain->findFirstBlockWithTimeAndHeight( | ||||
pindexRescan = ::ChainActive().Next(pindexRescan); | walletInstance->nTimeFirstKey - TIMESTAMP_WINDOW, | ||||
rescan_height)) { | |||||
rescan_height = *first_block; | |||||
} | |||||
} | } | ||||
nStart = GetTimeMillis(); | nStart = GetTimeMillis(); | ||||
{ | { | ||||
WalletRescanReserver reserver(walletInstance.get()); | WalletRescanReserver reserver(walletInstance.get()); | ||||
const CBlockIndex *stop_block, *failed_block; | |||||
if (!reserver.reserve() || | if (!reserver.reserve() || | ||||
(ScanResult::SUCCESS != | (ScanResult::SUCCESS != | ||||
walletInstance->ScanForWalletTransactions( | walletInstance | ||||
pindexRescan, nullptr, reserver, failed_block, stop_block, | ->ScanForWalletTransactions( | ||||
true))) { | locked_chain->getBlockHash(rescan_height), BlockHash(), | ||||
reserver, true /* update */) | |||||
.status)) { | |||||
InitError( | InitError( | ||||
_("Failed to rescan the wallet during initialization")); | _("Failed to rescan the wallet during initialization")); | ||||
return nullptr; | return nullptr; | ||||
} | } | ||||
} | } | ||||
walletInstance->WalletLogPrintf("Rescan completed in %15dms\n", | walletInstance->WalletLogPrintf("Rescan completed in %15dms\n", | ||||
GetTimeMillis() - nStart); | GetTimeMillis() - nStart); | ||||
walletInstance->ChainStateFlushed(::ChainActive().GetLocator()); | walletInstance->ChainStateFlushed(locked_chain->getLocator()); | ||||
walletInstance->database->IncrementUpdateCounter(); | walletInstance->database->IncrementUpdateCounter(); | ||||
// Restore wallet transaction metadata after -zapwallettxes=1 | // Restore wallet transaction metadata after -zapwallettxes=1 | ||||
if (gArgs.GetBoolArg("-zapwallettxes", false) && | if (gArgs.GetBoolArg("-zapwallettxes", false) && | ||||
gArgs.GetArg("-zapwallettxes", "1") != "2") { | gArgs.GetArg("-zapwallettxes", "1") != "2") { | ||||
WalletBatch batch(*walletInstance->database); | WalletBatch batch(*walletInstance->database); | ||||
for (const CWalletTx &wtxOld : vWtx) { | for (const CWalletTx &wtxOld : vWtx) { | ||||
▲ Show 20 Lines • Show All 57 Lines • ▼ Show 20 Lines | CKeyPool::CKeyPool(const CPubKey &vchPubKeyIn, bool internalIn) { | ||||
m_pre_split = false; | m_pre_split = false; | ||||
} | } | ||||
CWalletKey::CWalletKey(int64_t nExpires) { | CWalletKey::CWalletKey(int64_t nExpires) { | ||||
nTimeCreated = (nExpires ? GetTime() : 0); | nTimeCreated = (nExpires ? GetTime() : 0); | ||||
nTimeExpires = nExpires; | nTimeExpires = nExpires; | ||||
} | } | ||||
void CMerkleTx::SetMerkleBranch(const CBlockIndex *pindex, int posInBlock) { | void CMerkleTx::SetMerkleBranch(const BlockHash &block_hash, int posInBlock) { | ||||
// Update the tx's hashBlock | // Update the tx's hashBlock | ||||
hashBlock = pindex->GetBlockHash(); | hashBlock = block_hash; | ||||
// Set the position of the transaction in the block. | // Set the position of the transaction in the block. | ||||
nIndex = posInBlock; | nIndex = posInBlock; | ||||
} | } | ||||
int CMerkleTx::GetDepthInMainChain( | int CMerkleTx::GetDepthInMainChain( | ||||
interfaces::Chain::Lock &locked_chain) const { | interfaces::Chain::Lock &locked_chain) const { | ||||
if (hashUnset()) { | if (hashUnset()) { | ||||
return 0; | return 0; | ||||
} | } | ||||
AssertLockHeld(cs_main); | AssertLockHeld(cs_main); | ||||
// Find the block it claims to be in. | return locked_chain.getBlockDepth(hashBlock) * (nIndex == -1 ? -1 : 1); | ||||
CBlockIndex *pindex = LookupBlockIndex(hashBlock); | |||||
if (!pindex || !::ChainActive().Contains(pindex)) { | |||||
return 0; | |||||
} | |||||
return ((nIndex == -1) ? (-1) : 1) * | |||||
(::ChainActive().Height() - pindex->nHeight + 1); | |||||
} | } | ||||
int CMerkleTx::GetBlocksToMaturity( | int CMerkleTx::GetBlocksToMaturity( | ||||
interfaces::Chain::Lock &locked_chain) const { | interfaces::Chain::Lock &locked_chain) const { | ||||
if (!IsCoinBase()) { | if (!IsCoinBase()) { | ||||
return 0; | return 0; | ||||
} | } | ||||
▲ Show 20 Lines • Show All 104 Lines • Show Last 20 Lines |