diff --git a/src/consensus/consensus.h b/src/consensus/consensus.h --- a/src/consensus/consensus.h +++ b/src/consensus/consensus.h @@ -23,8 +23,10 @@ * (network rule). */ static const int64_t MAX_BLOCK_SIGOPS_PER_MB = 20000; -/** allowed number of signature check operations per transaction. */ +/** allowed number of parsed signature check operations per transaction. */ static const uint64_t MAX_TX_SIGOPS_COUNT = 20000; +/** allowed number of executed signature check operations per transaction. */ +static const int MAX_TX_SIGCHECKS_COUNT = 20000; /** * Coinbase transaction outputs can only be spent after this number of new * blocks (network rule). diff --git a/src/script/script_error.h b/src/script/script_error.h --- a/src/script/script_error.h +++ b/src/script/script_error.h @@ -80,6 +80,9 @@ ILLEGAL_FORKID, MUST_USE_FORKID, + /* Auxiliary errors (unused by interpreter) */ + TX_METRICS_LIMIT_EXCEEDED, + ERROR_COUNT, }; diff --git a/src/script/script_error.cpp b/src/script/script_error.cpp --- a/src/script/script_error.cpp +++ b/src/script/script_error.cpp @@ -104,6 +104,8 @@ return "Illegal use of SIGHASH_FORKID"; case ScriptError::MUST_USE_FORKID: return "Signature must use SIGHASH_FORKID"; + case ScriptError::TX_METRICS_LIMIT_EXCEEDED: + return "Transaction resources (SigChecks) exceeded"; case ScriptError::UNKNOWN: case ScriptError::ERROR_COUNT: default: diff --git a/src/validation.h b/src/validation.h --- a/src/validation.h +++ b/src/validation.h @@ -522,8 +522,53 @@ EXCLUSIVE_LOCKS_REQUIRED(cs_main); /** - * Closure representing one script verification. - * Note that this stores references to the spending transaction. + * Counts the consumption of transaction-level script resources during calls to + * CScriptChecks. Has two responsibilities: + * - Obtain an accurate count of sigChecks for valid transactions. + * - Also, if the metrics gets too high (over a hardcoded limit) then allow this + * to be detected, thereby enforcing a hardcoded per-tx consensus rule on + * accumulated metrics. + * + * Currently, just tracks sigchecks and makes sure total sigchecks count is <= + * MAX_TX_SIGCHECKS_COUNT. Note however that this limit is only in fact + * effective when the SCRIPT_REPORT_SIGCHECKS flag is enabled. + */ +class TransactionMetricsAccumulator { +public: + using CounterType = int; + +private: + std::atomic nSigChecks; + + void Set(CounterType _nSigChecks) { + nSigChecks.store(_nSigChecks, std::memory_order_relaxed); + } + +public: + TransactionMetricsAccumulator() { Set(0); } + void Reset() { Set(0); } + + // Accumulate metrics as obtained from VerifyScript; return true/false + // whether the resulting accumulation is still within limits. + bool CheckedAccumulate(ScriptExecutionMetrics metrics); + + // Obtain sigchecks count. This will be <= MAX_TX_SIGCHECKS_COUNT iff + // no CheckedAccumulate calls returned false. + CounterType GetSigChecks() { + return nSigChecks.load(std::memory_order_relaxed); + } +}; + +/** + * Triple responsibility class: + * - Mostly is a closure representing one input's script verification, but also, + * - (optionally) has responsibility to update the containing transaction's + * metrics accumulator, and, + * - (TODO) has responsibiltiy to update the containing block's metrics + * accumulator. + * + * Note that this stores references to the spending transaction and + * the accumulator. */ class CScriptCheck { private: @@ -536,19 +581,22 @@ ScriptError error; ScriptExecutionMetrics metrics; PrecomputedTransactionData txdata; + TransactionMetricsAccumulator *pTxAccumulator; public: CScriptCheck() : amount(), ptxTo(nullptr), nIn(0), nFlags(0), cacheStore(false), - error(ScriptError::UNKNOWN), txdata() {} + error(ScriptError::UNKNOWN), txdata(), pTxAccumulator(nullptr) {} CScriptCheck(const CScript &scriptPubKeyIn, const Amount amountIn, const CTransaction &txToIn, unsigned int nInIn, uint32_t nFlagsIn, bool cacheIn, - const PrecomputedTransactionData &txdataIn) + const PrecomputedTransactionData &txdataIn, + TransactionMetricsAccumulator *pTxAccumulatorIn = nullptr) : scriptPubKey(scriptPubKeyIn), amount(amountIn), ptxTo(&txToIn), nIn(nInIn), nFlags(nFlagsIn), cacheStore(cacheIn), - error(ScriptError::UNKNOWN), txdata(txdataIn) {} + error(ScriptError::UNKNOWN), txdata(txdataIn), + pTxAccumulator(pTxAccumulatorIn) {} bool operator()(); @@ -561,6 +609,7 @@ std::swap(cacheStore, check.cacheStore); std::swap(error, check.error); std::swap(txdata, check.txdata); + std::swap(pTxAccumulator, check.pTxAccumulator); } ScriptError GetScriptError() const { return error; } diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1183,12 +1183,37 @@ AddCoins(view, tx, nHeight); } +bool TransactionMetricsAccumulator::CheckedAccumulate( + ScriptExecutionMetrics metrics) { + // Sanity check: + // An input is at least 41 bytes large, and can contain up to + // MAX_OPS_PER_SCRIPT operations. Make sure the worst case can't overflow. + constexpr auto maxinputs_metrics = + std::numeric_limits::max() / MAX_OPS_PER_SCRIPT; + constexpr auto maxinputs_tx = MAX_TX_SIZE / 41; + static_assert(maxinputs_metrics > maxinputs_tx, + "metrics accumulator sigchecks mustn't overflow"); + + CounterType accum_before = + nSigChecks.fetch_add(metrics.nSigChecks, std::memory_order_relaxed); + + return (accum_before + metrics.nSigChecks) <= MAX_TX_SIGCHECKS_COUNT; +} + bool CScriptCheck::operator()() { const CScript &scriptSig = ptxTo->vin[nIn].scriptSig; - return VerifyScript(scriptSig, scriptPubKey, nFlags, - CachingTransactionSignatureChecker(ptxTo, nIn, amount, - cacheStore, txdata), - metrics, &error); + if (!VerifyScript(scriptSig, scriptPubKey, nFlags, + CachingTransactionSignatureChecker(ptxTo, nIn, amount, + cacheStore, txdata), + metrics, &error)) { + // error is filled in + return false; + } + if (pTxAccumulator && !pTxAccumulator->CheckedAccumulate(metrics)) { + error = ScriptError::TX_METRICS_LIMIT_EXCEEDED; + return false; + } + return true; } int GetSpendHeight(const CCoinsViewCache &inputs) {