Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .tx/config
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[main]
host = https://www.transifex.com

[dash.dash_ents]
[o:dash:p:dash:r:dash_ents]
file_filter = src/qt/locale/dash_<lang>.ts
source_file = src/qt/locale/dash_en.xlf
source_lang = en
1 change: 1 addition & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ BITCOIN_TESTS += wallet/test/db_tests.cpp
endif

FUZZ_WALLET_SRC = \
wallet/test/fuzz/coincontrol.cpp \
wallet/test/fuzz/coinselection.cpp

if USE_SQLITE
Expand Down
4 changes: 3 additions & 1 deletion src/test/miner_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ struct MinerTestingSetup : public TestingSetup {
bool TestSequenceLocks(const CTransaction& tx) EXCLUSIVE_LOCKS_REQUIRED(::cs_main, m_node.mempool->cs)
{
CCoinsViewMemPool view_mempool(&m_node.chainman->ActiveChainstate().CoinsTip(), *m_node.mempool);
return CheckSequenceLocksAtTip(m_node.chainman->ActiveChain().Tip(), view_mempool, tx);
CBlockIndex* tip{m_node.chainman->ActiveChain().Tip()};
const std::optional<LockPoints> lock_points{CalculateLockPointsAtTip(tip, view_mempool, tx)};
return lock_points.has_value() && CheckSequenceLocksAtTip(tip, *lock_points);
}
BlockAssembler AssemblerForTest(const CChainParams& params);
};
Expand Down
191 changes: 115 additions & 76 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,89 @@ bool CheckFinalTxAtTip(const CBlockIndex& active_chain_tip, const CTransaction&
return IsFinalTx(tx, nBlockHeight, nBlockTime);
}

namespace {
/**
* A helper which calculates heights of inputs of a given transaction.
*
* @param[in] tip The current chain tip. If an input belongs to a mempool
* transaction, we assume it will be confirmed in the next block.
* @param[in] coins Any CCoinsView that provides access to the relevant coins.
* @param[in] tx The transaction being evaluated.
*
* @returns A vector of input heights or nullopt, in case of an error.
*/
std::optional<std::vector<int>> CalculatePrevHeights(
const CBlockIndex& tip,
const CCoinsView& coins,
const CTransaction& tx)
{
std::vector<int> prev_heights;
prev_heights.resize(tx.vin.size());
for (size_t i = 0; i < tx.vin.size(); ++i) {
const CTxIn& txin = tx.vin[i];
Coin coin;
if (!coins.GetCoin(txin.prevout, coin)) {
LogPrintf("ERROR: %s: Missing input %d in transaction \'%s\'\n", __func__, i, tx.GetHash().GetHex());
return std::nullopt;
}
if (coin.nHeight == MEMPOOL_HEIGHT) {
// Assume all mempool transaction confirm in the next block.
prev_heights[i] = tip.nHeight + 1;
} else {
prev_heights[i] = coin.nHeight;
}
}
return prev_heights;
}
} // namespace

std::optional<LockPoints> CalculateLockPointsAtTip(
CBlockIndex* tip,
const CCoinsView& coins_view,
const CTransaction& tx)
{
assert(tip);

auto prev_heights{CalculatePrevHeights(*tip, coins_view, tx)};
if (!prev_heights.has_value()) return std::nullopt;

CBlockIndex next_tip;
next_tip.pprev = tip;
// When SequenceLocks() is called within ConnectBlock(), the height
// of the block *being* evaluated is what is used.
// Thus if we want to know if a transaction can be part of the
// *next* block, we need to use one more than active_chainstate.m_chain.Height()
next_tip.nHeight = tip->nHeight + 1;
const auto [min_height, min_time] = CalculateSequenceLocks(tx, STANDARD_LOCKTIME_VERIFY_FLAGS, prev_heights.value(), next_tip);

// Also store the hash of the block with the highest height of
// all the blocks which have sequence locked prevouts.
// This hash needs to still be on the chain
// for these LockPoint calculations to be valid
// Note: It is impossible to correctly calculate a maxInputBlock
// if any of the sequence locked inputs depend on unconfirmed txs,
// except in the special case where the relative lock time/height
// is 0, which is equivalent to no sequence lock. Since we assume
// input height of tip+1 for mempool txs and test the resulting
// min_height and min_time from CalculateSequenceLocks against tip+1.
int max_input_height{0};
for (const int height : prev_heights.value()) {
// Can ignore mempool inputs since we'll fail if they had non-zero locks
if (height != next_tip.nHeight) {
max_input_height = std::max(max_input_height, height);
}
}

// tip->GetAncestor(max_input_height) should never return a nullptr
// because max_input_height is always less than the tip height.
// It would, however, be a bad bug to continue execution, since a
// LockPoints object with the maxInputBlock member set to nullptr
// signifies no relative lock time.
return LockPoints{min_height, min_time, Assert(tip->GetAncestor(max_input_height))};
}
Comment on lines +261 to +267
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Replace Assert with proper error handling

Using Assert() at line 266 could cause a crash in production if GetAncestor() returns nullptr. While the comment suggests this shouldn't happen, defensive programming practices recommend proper error handling.

-    // tip->GetAncestor(max_input_height) should never return a nullptr
-    // because max_input_height is always less than the tip height.
-    // It would, however, be a bad bug to continue execution, since a
-    // LockPoints object with the maxInputBlock member set to nullptr
-    // signifies no relative lock time.
-    return LockPoints{min_height, min_time, Assert(tip->GetAncestor(max_input_height))};
+    // tip->GetAncestor(max_input_height) should never return a nullptr
+    // because max_input_height is always less than the tip height.
+    // It would, however, be a bad bug to continue execution, since a
+    // LockPoints object with the maxInputBlock member set to nullptr
+    // signifies no relative lock time.
+    CBlockIndex* max_input_block = tip->GetAncestor(max_input_height);
+    if (!max_input_block) {
+        LogPrintf("ERROR: %s: Failed to get ancestor at height %d for tip at height %d\n", 
+                  __func__, max_input_height, tip->nHeight);
+        return std::nullopt;
+    }
+    return LockPoints{min_height, min_time, max_input_block};
🤖 Prompt for AI Agents
In src/validation.cpp around lines 261 to 267, replace the use of Assert() on
the result of tip->GetAncestor(max_input_height) with proper error handling to
avoid potential crashes in production. Check if GetAncestor returns nullptr, and
if so, handle the error gracefully by returning an error code, throwing an
exception, or logging an error and returning a safe default LockPoints object
instead of proceeding with a nullptr.


bool CheckSequenceLocksAtTip(CBlockIndex* tip,
const CCoinsView& coins_view,
const CTransaction& tx,
LockPoints* lp,
bool useExistingLockPoints)
const LockPoints& lock_points)
{
assert(tip != nullptr);

Expand All @@ -203,61 +281,7 @@ bool CheckSequenceLocksAtTip(CBlockIndex* tip,
// *next* block, we need to use one more than active_chainstate.m_chain.Height()
index.nHeight = tip->nHeight + 1;

std::pair<int, int64_t> lockPair;
if (useExistingLockPoints) {
assert(lp);
lockPair.first = lp->height;
lockPair.second = lp->time;
}
else {
std::vector<int> prevheights;
prevheights.resize(tx.vin.size());
for (size_t txinIndex = 0; txinIndex < tx.vin.size(); txinIndex++) {
const CTxIn& txin = tx.vin[txinIndex];
Coin coin;
if (!coins_view.GetCoin(txin.prevout, coin)) {
return error("%s: Missing input", __func__);
}
if (coin.nHeight == MEMPOOL_HEIGHT) {
// Assume all mempool transaction confirm in the next block
prevheights[txinIndex] = tip->nHeight + 1;
} else {
prevheights[txinIndex] = coin.nHeight;
}
}
lockPair = CalculateSequenceLocks(tx, STANDARD_LOCKTIME_VERIFY_FLAGS, prevheights, index);
if (lp) {
lp->height = lockPair.first;
lp->time = lockPair.second;
// Also store the hash of the block with the highest height of
// all the blocks which have sequence locked prevouts.
// This hash needs to still be on the chain
// for these LockPoint calculations to be valid
// Note: It is impossible to correctly calculate a maxInputBlock
// if any of the sequence locked inputs depend on unconfirmed txs,
// except in the special case where the relative lock time/height
// is 0, which is equivalent to no sequence lock. Since we assume
// input height of tip+1 for mempool txs and test the resulting
// lockPair from CalculateSequenceLocks against tip+1. We know
// EvaluateSequenceLocks will fail if there was a non-zero sequence
// lock on a mempool input, so we can use the return value of
// CheckSequenceLocksAtTip to indicate the LockPoints validity
int maxInputHeight = 0;
for (const int height : prevheights) {
// Can ignore mempool inputs since we'll fail if they had non-zero locks
if (height != tip->nHeight+1) {
maxInputHeight = std::max(maxInputHeight, height);
}
}
// tip->GetAncestor(maxInputHeight) should never return a nullptr
// because maxInputHeight is always less than the tip height.
// It would, however, be a bad bug to continue execution, since a
// LockPoints object with the maxInputBlock member set to nullptr
// signifies no relative lock time.
lp->maxInputBlock = Assert(tip->GetAncestor(maxInputHeight));
}
}
return EvaluateSequenceLocks(index, lockPair);
return EvaluateSequenceLocks(index, {lock_points.height, lock_points.time});
}

bool GetUTXOCoin(CChainState& active_chainstate, const COutPoint& outpoint, Coin& coin)
Expand Down Expand Up @@ -405,20 +429,23 @@ void CChainState::MaybeUpdateMempoolForReorg(

// The transaction must be final.
if (!CheckFinalTxAtTip(*Assert(m_chain.Tip()), tx)) return true;
LockPoints lp = it->GetLockPoints();
const bool validLP{TestLockPointValidity(m_chain, lp)};
CCoinsViewMemPool view_mempool(&CoinsTip(), *m_mempool);

const LockPoints& lp = it->GetLockPoints();
// CheckSequenceLocksAtTip checks if the transaction will be final in the next block to be
// created on top of the new chain. We use useExistingLockPoints=false so that, instead of
// using the information in lp (which might now refer to a block that no longer exists in
// the chain), it will update lp to contain LockPoints relevant to the new chain.
if (!CheckSequenceLocksAtTip(m_chain.Tip(), view_mempool, tx, &lp, validLP)) {
// If CheckSequenceLocksAtTip fails, remove the tx and don't depend on the LockPoints.
return true;
} else if (!validLP) {
// If CheckSequenceLocksAtTip succeeded, it also updated the LockPoints.
// Now update the mempool entry lockpoints as well.
m_mempool->mapTx.modify(it, [&lp](CTxMemPoolEntry& e) { e.UpdateLockPoints(lp); });
// created on top of the new chain.
if (TestLockPointValidity(m_chain, lp)) {
if (!CheckSequenceLocksAtTip(m_chain.Tip(), lp)) {
return true;
}
} else {
const CCoinsViewMemPool view_mempool{&CoinsTip(), *m_mempool};
const std::optional<LockPoints> new_lock_points{CalculateLockPointsAtTip(m_chain.Tip(), view_mempool, tx)};
if (new_lock_points.has_value() && CheckSequenceLocksAtTip(m_chain.Tip(), *new_lock_points)) {
// Now update the mempool entry lockpoints as well.
m_mempool->mapTx.modify(it, [&new_lock_points](CTxMemPoolEntry& e) { e.UpdateLockPoints(*new_lock_points); });
} else {
return true;
}
}

// If the transaction spends any coinbase outputs, it must be mature.
Expand Down Expand Up @@ -781,7 +808,6 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
}
}
}
LockPoints lp;
m_view.SetBackend(m_viewmempool);

const CCoinsViewCache& coins_cache = m_active_chainstate.CoinsTip();
Expand Down Expand Up @@ -823,7 +849,8 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
// be mined yet.
// Pass in m_view which has all of the relevant inputs cached. Note that, since m_view's
// backend was removed, it no longer pulls coins from the mempool.
if (!CheckSequenceLocksAtTip(m_active_chainstate.m_chain.Tip(), m_view, tx, &lp)) {
const std::optional<LockPoints> lock_points{CalculateLockPointsAtTip(m_active_chainstate.m_chain.Tip(), m_view, tx)};
if (!lock_points.has_value() || !CheckSequenceLocksAtTip(m_active_chainstate.m_chain.Tip(), *lock_points)) {
return state.Invalid(TxValidationResult::TX_PREMATURE_SPEND, "non-BIP68-final");
}

Expand Down Expand Up @@ -854,7 +881,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
}

entry.reset(new CTxMemPoolEntry(ptx, ws.m_base_fees, nAcceptTime, m_active_chainstate.m_chain.Height(),
fSpendsCoinbase, nSigOps, lp));
fSpendsCoinbase, nSigOps, lock_points.value()));
ws.m_vsize = entry->GetTxSize();

if (nSigOps > MAX_STANDARD_TX_SIGOPS)
Expand Down Expand Up @@ -1930,11 +1957,21 @@ DisconnectResult CChainState::DisconnectBlock(const CBlock& block, const CBlockI
return DISCONNECT_FAILED;
}

// Ignore blocks that contain transactions which are 'overwritten' by later transactions,
// unless those are already completely spent.
// See https://github.com/bitcoin/bitcoin/issues/22596 for additional information.
// Note: the blocks specified here are different than the ones used in ConnectBlock because DisconnectBlock
// unwinds the blocks in reverse. As a result, the inconsistency is not discovered until the earlier
// blocks with the duplicate coinbase transactions are disconnected.
bool fEnforceBIP30 = !((pindex->nHeight==91722 && pindex->GetBlockHash() == uint256S("0x00000000000271a2dc26e7667f8419f2e15416dc6955e5a6c6cdf3f2574dd08e")) ||
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: these hard-coded blocks should not be backported IMO

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@UdjinM6 said we may as well backport them

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the reindex fails -> need to backport that specific blocks which fails. If re-index doesn't fail -> no need to backport any exception

(pindex->nHeight==91812 && pindex->GetBlockHash() == uint256S("0x00000000000af0aed4792b1acee3d966af36cf5def14935db8de83d6f9306f2f")));

// undo transactions in reverse order
for (int i = block.vtx.size() - 1; i >= 0; i--) {
const CTransaction &tx = *(block.vtx[i]);
uint256 hash = tx.GetHash();
bool is_coinbase = tx.IsCoinBase();
bool is_bip30_exception = (is_coinbase && !fEnforceBIP30);

if (fAddressIndex) {
for (unsigned int k = tx.vout.size(); k-- > 0;) {
Expand Down Expand Up @@ -1963,7 +2000,9 @@ DisconnectResult CChainState::DisconnectBlock(const CBlock& block, const CBlockI
Coin coin;
bool is_spent = view.SpendCoin(out, &coin);
if (!is_spent || tx.vout[o] != coin.out || pindex->nHeight != coin.nHeight || is_coinbase != coin.fCoinBase) {
fClean = false; // transaction output mismatch
if (!is_bip30_exception) {
fClean = false; // transaction output mismatch
}
}
}
}
Expand Down
36 changes: 24 additions & 12 deletions src/validation.h
Original file line number Diff line number Diff line change
Expand Up @@ -281,27 +281,39 @@ int GetUTXOConfirmations(CChainState& active_chainstate, const COutPoint& outpoi
bool CheckFinalTxAtTip(const CBlockIndex& active_chain_tip, const CTransaction& tx) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);

/**
* Check if transaction will be BIP68 final in the next block to be created on top of tip.
* @param[in] tip Chain tip to check tx sequence locks against. For example,
* the tip of the current active chain.
* Calculate LockPoints required to check if transaction will be BIP68 final in the next block
* to be created on top of tip.
*
* @param[in] tip Chain tip for which tx sequence locks are calculated. For
* example, the tip of the current active chain.
* @param[in] coins_view Any CCoinsView that provides access to the relevant coins for
* checking sequence locks. For example, it can be a CCoinsViewCache
* that isn't connected to anything but contains all the relevant
* coins, or a CCoinsViewMemPool that is connected to the
* mempool and chainstate UTXO set. In the latter case, the caller is
* responsible for holding the appropriate locks to ensure that
* mempool and chainstate UTXO set. In the latter case, the caller
* is responsible for holding the appropriate locks to ensure that
* calls to GetCoin() return correct coins.
* @param[in] tx The transaction being evaluated.
*
* @returns The resulting height and time calculated and the hash of the block needed for
* calculation, or std::nullopt if there is an error.
*/
std::optional<LockPoints> CalculateLockPointsAtTip(
CBlockIndex* tip,
const CCoinsView& coins_view,
const CTransaction& tx);

/**
* Check if transaction will be BIP68 final in the next block to be created on top of tip.
* @param[in] tip Chain tip to check tx sequence locks against. For example,
* the tip of the current active chain.
* @param[in] lock_points LockPoints containing the height and time at which this
* transaction is final.
* Simulates calling SequenceLocks() with data from the tip passed in.
* Optionally stores in LockPoints the resulting height and time calculated and the hash
* of the block needed for calculation or skips the calculation and uses the LockPoints
* passed in for evaluation.
* The LockPoints should not be considered valid if CheckSequenceLocksAtTip returns false.
*/
bool CheckSequenceLocksAtTip(CBlockIndex* tip,
const CCoinsView& coins_view,
const CTransaction& tx,
LockPoints* lp = nullptr,
bool useExistingLockPoints = false);
const LockPoints& lock_points);

/**
* Closure representing one script verification
Expand Down
2 changes: 1 addition & 1 deletion src/wallet/spend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ static std::optional<CreatedTransactionResult> CreateTransactionInternal(
}
if (feeCalc.reason == FeeReason::FALLBACK && !wallet.m_allow_fallback_fee) {
// eventually allow a fallback fee
error = _("Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable -fallbackfee.");
error = strprintf(_("Fee estimation failed. Fallbackfee is disabled. Wait a few blocks or enable %s."), "-fallbackfee");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

25666 - consider follow-up to apply it for dash's strings;

I found several of them:

        error = Untranslated("You should use upgradetohd RPC to upgrade non-HD wallet to HD");
        error = strprintf(_("Cannot upgrade a non HD wallet from version %i to version %i which is non-HD wallet. Use upgradetohd RPC"), prev_version, version);

        return InitError(Untranslated("-zapwallettxes has been removed. If you are attempting to remove a stuck transaction from your wallet, please use abandontransaction instead."));
        return InitError(Untranslated("-sysperms is not allowed in combination with enabled wallet functionality"));
            errorStr = strprintf(_("%s corrupt. Try using the wallet tool dash-wallet to salvage or restoring a backup."), fs::quoted(fs::PathToString(file_path)));

        error = _("No dump file provided. To use dump, -dumpfile=<filename> must be provided.");

        error = _("No wallet file format provided. To use createfromdump, -format=<format> must be provided.");

        InitWarning(_("Incorrect -rescan mode, falling back to default value"));

            chain.initError(strprintf(_("Specified -walletdir \"%s\" does not exist"), fs::PathToString(wallet_dir)));

            chain.initError(strprintf(_("Specified -walletdir \"%s\" is not a directory"), fs::PathToString(wallet_dir)));

            chain.initError(strprintf(_("Specified -walletdir \"%s\" is a relative path"), fs::PathToString(wallet_dir)));

            chain.initWarning(strprintf(_("Ignoring duplicate -wallet %s."), wallet_file));

                chain.initWarning(Untranslated(strprintf("Skipping -wallet path that doesn't exist. %s", error_string.original)));

and one extra with forgotten dashification!

        error = strprintf(_("Error: Dumpfile version is not supported. This version of bitcoin-wallet only supports version 1 dumpfiles. Got dumpfile with version %s"), version_value);

return std::nullopt;
}

Expand Down
8 changes: 5 additions & 3 deletions src/wallet/sqlite.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@
namespace wallet {
static constexpr int32_t WALLET_SCHEMA_VERSION = 0;

static Mutex g_sqlite_mutex;
static int g_sqlite_count GUARDED_BY(g_sqlite_mutex) = 0;

static Span<const std::byte> SpanFromBlob(sqlite3_stmt* stmt, int col)
{
return {reinterpret_cast<const std::byte*>(sqlite3_column_blob(stmt, col)),
Expand Down Expand Up @@ -104,6 +101,9 @@ static void SetPragma(sqlite3* db, const std::string& key, const std::string& va
}
}

Mutex SQLiteDatabase::g_sqlite_mutex;
int SQLiteDatabase::g_sqlite_count = 0;

SQLiteDatabase::SQLiteDatabase(const fs::path& dir_path, const fs::path& file_path, const DatabaseOptions& options, bool mock)
: WalletDatabase(), m_mock(mock), m_dir_path(fs::PathToString(dir_path)), m_file_path(fs::PathToString(file_path)), m_use_unsafe_sync(options.use_unsafe_sync)
{
Expand Down Expand Up @@ -167,6 +167,8 @@ SQLiteDatabase::~SQLiteDatabase()

void SQLiteDatabase::Cleanup() noexcept
{
AssertLockNotHeld(g_sqlite_mutex);

Close();

LOCK(g_sqlite_mutex);
Expand Down
12 changes: 11 additions & 1 deletion src/wallet/sqlite.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#ifndef BITCOIN_WALLET_SQLITE_H
#define BITCOIN_WALLET_SQLITE_H

#include <sync.h>
#include <wallet/db.h>

#include <sqlite3.h>
Expand Down Expand Up @@ -63,7 +64,16 @@ class SQLiteDatabase : public WalletDatabase

const std::string m_file_path;

void Cleanup() noexcept;
/**
* This mutex protects SQLite initialization and shutdown.
* sqlite3_config() and sqlite3_shutdown() are not thread-safe (sqlite3_initialize() is).
* Concurrent threads that execute SQLiteDatabase::SQLiteDatabase() should have just one
* of them do the init and the rest wait for it to complete before all can proceed.
*/
static Mutex g_sqlite_mutex;
static int g_sqlite_count GUARDED_BY(g_sqlite_mutex);

void Cleanup() noexcept EXCLUSIVE_LOCKS_REQUIRED(!g_sqlite_mutex);

public:
SQLiteDatabase() = delete;
Expand Down
Loading
Loading