Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
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
177 changes: 102 additions & 75 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
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
89 changes: 89 additions & 0 deletions src/wallet/test/fuzz/coincontrol.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) 2022 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <test/fuzz/FuzzedDataProvider.h>
#include <test/fuzz/fuzz.h>
#include <test/fuzz/util.h>
#include <test/util/setup_common.h>
#include <wallet/coincontrol.h>
#include <wallet/test/util.h>

namespace wallet {
namespace {

const TestingSetup* g_setup;

void initialize_coincontrol()
{
static const auto testing_setup = MakeNoLogFileContext<const TestingSetup>();
g_setup = testing_setup.get();
}

FUZZ_TARGET(coincontrol, .init = initialize_coincontrol)
{
FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
const auto& node = g_setup->m_node;
ArgsManager& args = *node.args;

// for GetBoolArg to return true sometimes
args.ForceSetArg("-avoidpartialspends", fuzzed_data_provider.ConsumeBool()?"1":"0");

CCoinControl coin_control;
COutPoint out_point;

LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000)
{
CallOneOf(
fuzzed_data_provider,
[&] {
std::optional<COutPoint> optional_out_point = ConsumeDeserializable<COutPoint>(fuzzed_data_provider);
if (!optional_out_point) {
return;
}
out_point = *optional_out_point;
},
[&] {
(void)coin_control.HasSelected();
},
[&] {
(void)coin_control.IsSelected(out_point);
},
[&] {
(void)coin_control.IsExternalSelected(out_point);
},
[&] {
CTxOut txout;
(void)coin_control.GetExternalOutput(out_point, txout);
},
[&] {
(void)coin_control.Select(out_point);
},
[&] {
const CTxOut tx_out{ConsumeMoney(fuzzed_data_provider), ConsumeScript(fuzzed_data_provider)};
(void)coin_control.SelectExternal(out_point, tx_out);
},
[&] {
(void)coin_control.UnSelect(out_point);
},
[&] {
(void)coin_control.UnSelectAll();
},
[&] {
std::vector<COutPoint> selected;
coin_control.ListSelected(selected);
},
[&] {
int64_t weight{fuzzed_data_provider.ConsumeIntegral<int64_t>()};
(void)coin_control.SetInputWeight(out_point, weight);
Copy link
Collaborator

Choose a reason for hiding this comment

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

27902: should skip this line IMO and next one, because it's segwit related. I will prepare fix to remove GetInputWeight and HasInputWeight from coin_control

Copy link
Member Author

Choose a reason for hiding this comment

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

Why? What harm does keeping it do? it seems it just keeps our code closer to upstream w/o downside

Copy link
Collaborator

Choose a reason for hiding this comment

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

because we use everywhere a size of transaction instead of "weight".

Having leftover helpers with HasInputWeight and GetInpuptWeight can cause an error in future, if some new (especially backported) code will use it. If these helpers won't exist, freshly backported code will get compilation error -> a developer got a signal to replace "weight" to "size". Otherwise it maybe a silent bug

},
[&] {
// Condition to avoid the assertion in GetInputWeight
if (coin_control.HasInputWeight(out_point)) {
(void)coin_control.GetInputWeight(out_point);
}
});
}
}
} // namespace
} // namespace wallet
Loading