Skip to content
Open
5 changes: 4 additions & 1 deletion src/bench/connectblock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <script/interpreter.h>
#include <sync.h>
#include <test/util/setup_common.h>
#include <undo.h>
#include <validation.h>

#include <cassert>
Expand Down Expand Up @@ -100,7 +101,9 @@ void BenchmarkConnectBlock(benchmark::Bench& bench, std::vector<CKey>& keys, std
auto* pindex{chainman->m_blockman.AddToBlockIndex(test_block, chainman->m_best_header)}; // Doing this here doesn't impact the benchmark
CCoinsViewCache viewNew{&chainstate.CoinsTip()};

assert(chainstate.ConnectBlock(test_block, test_block_state, pindex, viewNew));
CBlockUndo blockundo;
assert(chainstate.SpendBlock(test_block, pindex, viewNew, test_block_state, blockundo));
assert(chainstate.ConnectBlock(test_block, blockundo, test_block_state, pindex));
});
}

Expand Down
13 changes: 13 additions & 0 deletions src/coins.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,19 @@ const Coin& CCoinsViewCache::AccessCoin(const COutPoint &outpoint) const {
}
}

std::vector<CTxOut> CCoinsViewCache::GetUnspentOutputs(const CTransaction& tx) const
{
std::vector<CTxOut> spent_outputs;
spent_outputs.reserve(tx.vin.size());
for (const auto& txin : tx.vin) {
const COutPoint& prevout = txin.prevout;
const Coin& coin = AccessCoin(prevout);
assert(!coin.IsSpent());
spent_outputs.emplace_back(coin.out);
}
return spent_outputs;
}

bool CCoinsViewCache::HaveCoin(const COutPoint& outpoint) const
{
CCoinsMap::const_iterator it = FetchCoin(outpoint);
Expand Down
22 changes: 22 additions & 0 deletions src/coins.h
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,28 @@ class CCoinsViewCache : public CCoinsViewBacked
*/
const Coin& AccessCoin(const COutPoint &output) const;

/**
* Retrieves a vector of references to Coins in the cache in order they get
* consumed by the tx's inputs, and passes it to the invoked callback.
*/
template <typename Callable>
auto AccessCoins(const CTransaction& tx, Callable&& callback) const
{
std::vector<std::reference_wrapper<const Coin>> coins;
coins.reserve(tx.vin.size());
for (const CTxIn& input : tx.vin) {
coins.emplace_back(AccessCoin(input.prevout));
}
return callback(std::span{coins});
}

/**
* Return a vector of unspent outputs of coins in the cache that are spent
* by the provided transaction. The coins they belong to must be unspent in
* the cache.
*/
std::vector<CTxOut> GetUnspentOutputs(const CTransaction& tx) const;

/**
* Add a coin. Set possible_overwrite to true if an unspent version may
* already exist in the cache.
Expand Down
63 changes: 41 additions & 22 deletions src/consensus/tx_verify.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
#include <util/check.h>
#include <util/moneystr.h>

#include <span>

bool IsFinalTx(const CTransaction &tx, int nBlockHeight, int64_t nBlockTime)
{
if (tx.nLockTime == 0)
Expand Down Expand Up @@ -123,62 +125,73 @@ unsigned int GetLegacySigOpCount(const CTransaction& tx)
return nSigOps;
}

unsigned int GetP2SHSigOpCount(const CTransaction& tx, const CCoinsViewCache& inputs)
template <Consensus::CoinRef T>
unsigned int GetP2SHSigOpCount(const CTransaction& tx, const std::span<T> coins)
{
if (tx.IsCoinBase())
return 0;

unsigned int nSigOps = 0;
for (unsigned int i = 0; i < tx.vin.size(); i++)
{
const Coin& coin = inputs.AccessCoin(tx.vin[i].prevout);
Assert(coins.size() == tx.vin.size());
auto input_it = tx.vin.begin();
for (auto it = coins.begin(); it != coins.end(); ++it, ++input_it) {
const Coin& coin = *it;
assert(!coin.IsSpent());
const CTxOut &prevout = coin.out;
if (prevout.scriptPubKey.IsPayToScriptHash())
nSigOps += prevout.scriptPubKey.GetSigOpCount(tx.vin[i].scriptSig);
nSigOps += prevout.scriptPubKey.GetSigOpCount(input_it->scriptSig);
}
return nSigOps;
}

int64_t GetTransactionSigOpCost(const CTransaction& tx, const CCoinsViewCache& inputs, script_verify_flags flags)
template unsigned int GetP2SHSigOpCount<const Coin>(
const CTransaction& tx, const std::span<const Coin>);

template unsigned int GetP2SHSigOpCount<std::reference_wrapper<const Coin>>(
const CTransaction& tx, const std::span<std::reference_wrapper<const Coin>>);

template <Consensus::CoinRef T>
int64_t GetTransactionSigOpCost(const CTransaction& tx, const std::span<T> coins, script_verify_flags flags)
{
int64_t nSigOps = GetLegacySigOpCount(tx) * WITNESS_SCALE_FACTOR;

if (tx.IsCoinBase())
return nSigOps;

if (flags & SCRIPT_VERIFY_P2SH) {
nSigOps += GetP2SHSigOpCount(tx, inputs) * WITNESS_SCALE_FACTOR;
nSigOps += GetP2SHSigOpCount(tx, coins) * WITNESS_SCALE_FACTOR;
}

for (unsigned int i = 0; i < tx.vin.size(); i++)
{
const Coin& coin = inputs.AccessCoin(tx.vin[i].prevout);
Assert(coins.size() == tx.vin.size());
auto input_it = tx.vin.begin();
for (auto it = coins.begin(); it != coins.end(); ++it, ++input_it) {
const Coin& coin = *it;
assert(!coin.IsSpent());
const CTxOut &prevout = coin.out;
nSigOps += CountWitnessSigOps(tx.vin[i].scriptSig, prevout.scriptPubKey, tx.vin[i].scriptWitness, flags);
nSigOps += CountWitnessSigOps(input_it->scriptSig, prevout.scriptPubKey, input_it->scriptWitness, flags);
}
return nSigOps;
}
template int64_t GetTransactionSigOpCost<const Coin>(
const CTransaction& tx, std::span<const Coin> coins, script_verify_flags flags);

bool Consensus::CheckTxInputs(const CTransaction& tx, TxValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& txfee)
{
// are the actual inputs available?
if (!inputs.HaveInputs(tx)) {
return state.Invalid(TxValidationResult::TX_MISSING_INPUTS, "bad-txns-inputs-missingorspent",
strprintf("%s: inputs missing/spent", __func__));
}
template int64_t GetTransactionSigOpCost<std::reference_wrapper<const Coin>>(
const CTransaction& tx, const std::span<std::reference_wrapper<const Coin>> coins, script_verify_flags flags);

template <Consensus::CoinRef T>
bool Consensus::CheckTxInputs(const CTransaction& tx, TxValidationState& state, const std::span<T> coins, int nSpendHeight, CAmount& txfee)
{
CAmount nValueIn = 0;
for (unsigned int i = 0; i < tx.vin.size(); ++i) {
const COutPoint &prevout = tx.vin[i].prevout;
const Coin& coin = inputs.AccessCoin(prevout);
Assert(coins.size() == tx.vin.size());
auto input_it = tx.vin.begin();
for (auto it = coins.begin(); it != coins.end(); ++it, ++input_it) {
const Coin& coin = *it;
assert(!coin.IsSpent());

// If prev is coinbase, check that it's matured
if (coin.IsCoinBase() && nSpendHeight - coin.nHeight < COINBASE_MATURITY) {
return state.Invalid(TxValidationResult::TX_PREMATURE_SPEND, "bad-txns-premature-spend-of-coinbase",
strprintf("tried to spend coinbase at depth %d", nSpendHeight - coin.nHeight));
strprintf("tried to spend coinbase at depth %d", static_cast<int>(nSpendHeight - coin.nHeight)));
}

// Check for negative or overflow input values
Expand Down Expand Up @@ -212,3 +225,9 @@ bool Consensus::CheckTxInputs(const CTransaction& tx, TxValidationState& state,
txfee = txfee_aux;
return true;
}

template bool Consensus::CheckTxInputs<const Coin>(
const CTransaction& tx, TxValidationState& state, const std::span<const Coin> coins, int nSpendHeight, CAmount& txfee);

template bool Consensus::CheckTxInputs<std::reference_wrapper<const Coin>>(
const CTransaction& tx, TxValidationState& state, const std::span<std::reference_wrapper<const Coin>> coins, int nSpendHeight, CAmount& txfee);
25 changes: 18 additions & 7 deletions src/consensus/tx_verify.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
#ifndef BITCOIN_CONSENSUS_TX_VERIFY_H
#define BITCOIN_CONSENSUS_TX_VERIFY_H

#include <coins.h>
#include <consensus/amount.h>
#include <script/verify_flags.h>

#include <concepts>
#include <cstdint>
#include <span>
#include <vector>

class CBlockIndex;
Expand All @@ -19,13 +22,19 @@ class TxValidationState;
/** Transaction validation functions */

namespace Consensus {

template <typename T>
concept CoinRef = std::convertible_to<T, const Coin&>;

/**
* Check whether all inputs of this transaction are valid (no double spends and amounts)
* Check whether all inputs of this transaction are valid (amounts and maturity)
* This does not modify the UTXO set. This does not check scripts and sigs.
* @param[in] coins span of Coins containing previous transaction outputs in the order we're spending them
* @param[out] txfee Set to the transaction fee if successful.
* Preconditions: tx.IsCoinBase() is false.
*/
[[nodiscard]] bool CheckTxInputs(const CTransaction& tx, TxValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& txfee);
template <Consensus::CoinRef T>
[[nodiscard]] bool CheckTxInputs(const CTransaction& tx, TxValidationState& state, std::span<T> coins, int nSpendHeight, CAmount& txfee);
} // namespace Consensus

/** Auxiliary functions for transaction validation (ideally should not be exposed) */
Expand All @@ -40,20 +49,22 @@ unsigned int GetLegacySigOpCount(const CTransaction& tx);
/**
* Count ECDSA signature operations in pay-to-script-hash inputs.
*
* @param[in] mapInputs Map of previous transactions that have outputs we're spending
* @param[in] coins span of Coins containing previous transaction outputs in the order we're spending them
* @return maximum number of sigops required to validate this transaction's inputs
* @see CTransaction::FetchInputs
*/
unsigned int GetP2SHSigOpCount(const CTransaction& tx, const CCoinsViewCache& mapInputs);
template <Consensus::CoinRef T>
unsigned int GetP2SHSigOpCount(const CTransaction& tx, std::span<T> coins);

/**
* Compute total signature operation cost of a transaction.
* @param[in] tx Transaction for which we are computing the cost
* @param[in] inputs Map of previous transactions that have outputs we're spending
* @param[in] tx Transaction for which we are computing the cost
* @param[in] coins span of Coins containing previous transaction outputs in the order we're spending them
* @param[in] flags Script verification flags
* @return Total signature operation cost of tx
*/
int64_t GetTransactionSigOpCost(const CTransaction& tx, const CCoinsViewCache& inputs, script_verify_flags flags);
template <Consensus::CoinRef T>
int64_t GetTransactionSigOpCost(const CTransaction& tx, std::span<T> coins, script_verify_flags flags);

/**
* Check if transaction is final and can be included in a block with the
Expand Down
5 changes: 4 additions & 1 deletion src/node/psbt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,10 @@ PSBTAnalysis AnalyzePSBT(PartiallySignedTransaction psbtx)

if (success) {
CTransaction ctx = CTransaction(mtx);
size_t size(GetVirtualTransactionSize(ctx, GetTransactionSigOpCost(ctx, view, STANDARD_SCRIPT_VERIFY_FLAGS), ::nBytesPerSigOp));
auto tx_ops = view.AccessCoins(ctx, [&ctx](auto&& coins) {
return GetTransactionSigOpCost(ctx, coins, STANDARD_SCRIPT_VERIFY_FLAGS);
});
size_t size(GetVirtualTransactionSize(ctx, tx_ops, ::nBytesPerSigOp));
result.estimated_vsize = size;
// Estimate fee rate
CFeeRate feerate(fee, size);
Expand Down
5 changes: 4 additions & 1 deletion src/test/coinstatsindex_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <kernel/types.h>
#include <test/util/setup_common.h>
#include <test/util/validation.h>
#include <undo.h>
#include <util/byte_units.h>
#include <validation.h>

Expand Down Expand Up @@ -92,7 +93,9 @@ BOOST_FIXTURE_TEST_CASE(coinstatsindex_unclean_shutdown, TestChain100Setup)
BOOST_CHECK(CheckBlock(block, state, params.GetConsensus()));
BOOST_CHECK(m_node.chainman->AcceptBlock(new_block, state, &new_block_index, true, nullptr, nullptr, true));
CCoinsViewCache view(&chainstate.CoinsTip());
BOOST_CHECK(chainstate.ConnectBlock(block, state, new_block_index, view));
CBlockUndo blockundo;
BOOST_CHECK(chainstate.SpendBlock(block, new_block_index, view, state, blockundo));
BOOST_CHECK(chainstate.ConnectBlock(block, blockundo, state, new_block_index));
}
// Send block connected notification, then stop the index without
// sending a chainstate flushed notification. Prior to #24138, this
Expand Down
13 changes: 10 additions & 3 deletions src/test/fuzz/coins_view.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,12 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& co
// It is not allowed to call CheckTxInputs if CheckTransaction failed
return;
}
if (Consensus::CheckTxInputs(transaction, state, coins_view_cache, fuzzed_data_provider.ConsumeIntegralInRange<int>(0, std::numeric_limits<int>::max()), tx_fee_out)) {
// are the actual inputs available?
if (!coins_view_cache.HaveInputs(transaction)) return;
auto check_tx_inputs_res = coins_view_cache.AccessCoins(transaction, [&transaction, &state, &fuzzed_data_provider, &tx_fee_out](auto&& coins) {
return Consensus::CheckTxInputs(transaction, state, coins, fuzzed_data_provider.ConsumeIntegralInRange<int>(0, std::numeric_limits<int>::max()), tx_fee_out);
});
if (check_tx_inputs_res) {
assert(MoneyRange(tx_fee_out));
}
},
Expand All @@ -299,7 +304,7 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& co
// consensus/tx_verify.cpp:130: unsigned int GetP2SHSigOpCount(const CTransaction &, const CCoinsViewCache &): Assertion `!coin.IsSpent()' failed.
return;
}
(void)GetP2SHSigOpCount(transaction, coins_view_cache);
(void)coins_view_cache.AccessCoins(transaction, [&transaction](auto&& coins) { return GetP2SHSigOpCount(transaction, coins); });
},
[&] {
const CTransaction transaction{random_mutable_transaction};
Expand All @@ -314,7 +319,9 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& co
// script/interpreter.cpp:1705: size_t CountWitnessSigOps(const CScript &, const CScript &, const CScriptWitness &, unsigned int): Assertion `(flags & SCRIPT_VERIFY_P2SH) != 0' failed.
return;
}
(void)GetTransactionSigOpCost(transaction, coins_view_cache, flags);
(void)coins_view_cache.AccessCoins(transaction, [&transaction, &flags](auto&& coins) {
return GetTransactionSigOpCost(transaction, coins, flags);
});
},
[&] {
(void)IsWitnessStandard(CTransaction{random_mutable_transaction}, coins_view_cache);
Expand Down
14 changes: 9 additions & 5 deletions src/test/script_p2sh_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -378,13 +378,14 @@ BOOST_AUTO_TEST_CASE(ValidateInputsStandardness)

BOOST_CHECK(::ValidateInputsStandardness(CTransaction(txTo), coins).IsValid());
// 22 P2SH sigops for all inputs (1 for vin[0], 6 for vin[3], 15 for vin[4]
BOOST_CHECK_EQUAL(GetP2SHSigOpCount(CTransaction(txTo), coins), 22U);
auto count = coins.AccessCoins(CTransaction(txTo), [&txTo](auto&& coins) { return GetP2SHSigOpCount(CTransaction(txTo), coins); });
BOOST_CHECK_EQUAL(count, 22U);

CMutableTransaction coinbase_tx_mut;
coinbase_tx_mut.vin.resize(1);
CTransaction coinbase_tx{coinbase_tx_mut};
BOOST_CHECK(coinbase_tx.IsCoinBase());
BOOST_CHECK_EQUAL(GetP2SHSigOpCount(coinbase_tx, coins), 0U);
BOOST_CHECK_EQUAL(GetP2SHSigOpCount(coinbase_tx, std::span<const Coin>{}), 0U);

// TxoutType::SCRIPTHASH
CMutableTransaction txToNonStd1;
Expand All @@ -401,7 +402,8 @@ BOOST_AUTO_TEST_CASE(ValidateInputsStandardness)
BOOST_CHECK_EQUAL(txToNonStd1_res.GetRejectReason(), "bad-txns-nonstandard-inputs");
BOOST_CHECK_EQUAL(txToNonStd1_res.GetDebugMessage(), "p2sh redeemscript sigops exceed limit (input 0: 16 > 15)");

BOOST_CHECK_EQUAL(GetP2SHSigOpCount(CTransaction(txToNonStd1), coins), 16U);
count = coins.AccessCoins(CTransaction(txToNonStd1), [&txToNonStd1](auto&& coins) { return GetP2SHSigOpCount(CTransaction(txToNonStd1), coins); });
BOOST_CHECK_EQUAL(count, 16U);

CMutableTransaction txToNonStd2;
txToNonStd2.vout.resize(1);
Expand All @@ -416,7 +418,8 @@ BOOST_AUTO_TEST_CASE(ValidateInputsStandardness)
BOOST_CHECK(txToNonStd2_res.IsInvalid());
BOOST_CHECK_EQUAL(txToNonStd2_res.GetRejectReason(), "bad-txns-nonstandard-inputs");
BOOST_CHECK_EQUAL(txToNonStd2_res.GetDebugMessage(), "p2sh redeemscript sigops exceed limit (input 0: 20 > 15)");
BOOST_CHECK_EQUAL(GetP2SHSigOpCount(CTransaction(txToNonStd2), coins), 20U);
count = coins.AccessCoins(CTransaction(txToNonStd2), [&txToNonStd2](auto&& coins) { return GetP2SHSigOpCount(CTransaction(txToNonStd2), coins); });
BOOST_CHECK_EQUAL(count, 20U);

CMutableTransaction txToNonStd2_no_scriptSig;
txToNonStd2_no_scriptSig.vout.resize(1);
Expand All @@ -430,7 +433,8 @@ BOOST_AUTO_TEST_CASE(ValidateInputsStandardness)
BOOST_CHECK(txToNonStd2_no_scriptSig_res.IsInvalid());
BOOST_CHECK_EQUAL(txToNonStd2_no_scriptSig_res.GetRejectReason(), "bad-txns-nonstandard-inputs");
BOOST_CHECK_EQUAL(txToNonStd2_no_scriptSig_res.GetDebugMessage(), "input 0 P2SH redeemscript missing");
BOOST_CHECK_EQUAL(GetP2SHSigOpCount(CTransaction(txToNonStd2), coins), 20U);
count = coins.AccessCoins(CTransaction(txToNonStd2), [&txToNonStd2](auto&& coins) { return GetP2SHSigOpCount(CTransaction(txToNonStd2), coins); });
BOOST_CHECK_EQUAL(count, 20U);

// TxoutType::NONSTANDARD
CMutableTransaction txToNonStd3;
Expand Down
Loading
Loading