diff --git a/src/Makefile.test.include b/src/Makefile.test.include --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -114,6 +114,7 @@ test/sigencoding_tests.cpp \ test/sighash_tests.cpp \ test/sighashtype_tests.cpp \ + test/sigcheckcount_tests.cpp \ test/sigopcount_tests.cpp \ test/sigutil.h \ test/skiplist_tests.cpp \ diff --git a/src/script/interpreter.h b/src/script/interpreter.h --- a/src/script/interpreter.h +++ b/src/script/interpreter.h @@ -82,9 +82,24 @@ using MutableTransactionSignatureChecker = GenericTransactionSignatureChecker<CMutableTransaction>; +/** + * Struct for holding cumulative results from executing a script or a sequence + * of scripts. + */ +struct ScriptExecutionMetrics { + int nSigChecks = 0; +}; + bool EvalScript(std::vector<std::vector<uint8_t>> &stack, const CScript &script, uint32_t flags, const BaseSignatureChecker &checker, - ScriptError *error = nullptr); + ScriptExecutionMetrics &metrics, ScriptError *error = nullptr); +static inline bool EvalScript(std::vector<std::vector<uint8_t>> &stack, + const CScript &script, uint32_t flags, + const BaseSignatureChecker &checker, + ScriptError *error = nullptr) { + ScriptExecutionMetrics dummymetrics; + return EvalScript(stack, script, flags, checker, dummymetrics, error); +} bool VerifyScript(const CScript &scriptSig, const CScript &scriptPubKey, uint32_t flags, const BaseSignatureChecker &checker, ScriptError *serror = nullptr); diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -102,7 +102,7 @@ bool EvalScript(std::vector<valtype> &stack, const CScript &script, uint32_t flags, const BaseSignatureChecker &checker, - ScriptError *serror) { + ScriptExecutionMetrics &metrics, ScriptError *serror) { static const CScriptNum bnZero(0); static const CScriptNum bnOne(1); static const valtype vchFalse(0); @@ -885,6 +885,7 @@ fSuccess = checker.CheckSig(vchSig, vchPubKey, scriptCode, flags); + metrics.nSigChecks += 1; if (!fSuccess && (flags & SCRIPT_VERIFY_NULLFAIL)) { return set_error(serror, @@ -932,6 +933,7 @@ .Finalize(vchHash.data()); fSuccess = checker.VerifySignature( vchSig, CPubKey(vchPubKey), uint256(vchHash)); + metrics.nSigChecks += 1; if (!fSuccess && (flags & SCRIPT_VERIFY_NULLFAIL)) { return set_error(serror, @@ -1086,6 +1088,10 @@ return set_error(serror, ScriptError::SIG_NULLFAIL); } + + // this is guaranteed to execute exactly + // nSigsCount times (if not script error) + metrics.nSigChecks += 1; } if ((checkBits >> iKey) != 0) { @@ -1157,6 +1163,14 @@ return set_error(serror, ScriptError::SIG_NULLFAIL); } + + if (!areAllSignaturesNull) { + // This is not identical to the number of actual + // ECDSA verifies, but, it is an upper bound + // that can be easily determined without doing + // CPU-intensive checks. + metrics.nSigChecks += nKeysCount; + } } // Clean up stack of all arguments diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -135,6 +135,7 @@ sigencoding_tests.cpp sighash_tests.cpp sighashtype_tests.cpp + sigcheckcount_tests.cpp sigopcount_tests.cpp skiplist_tests.cpp streams_tests.cpp diff --git a/src/test/sigcheckcount_tests.cpp b/src/test/sigcheckcount_tests.cpp new file mode 100644 --- /dev/null +++ b/src/test/sigcheckcount_tests.cpp @@ -0,0 +1,308 @@ +// Copyright (c) 2019 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include <policy/policy.h> +#include <pubkey.h> +#include <script/interpreter.h> +#include <script/script_error.h> + +#include <test/test_bitcoin.h> + +#include <boost/test/unit_test.hpp> + +typedef std::vector<uint8_t> valtype; +typedef std::vector<valtype> stacktype; + +BOOST_FIXTURE_TEST_SUITE(sigcheckcount_tests, BasicTestingSetup) + +/** + * Stand-in for proper signature check, in the absence of a proper + * transaction context. We will use a dummy signature checker with + * placeholder signatures / pubkeys that are correctly encoded. + */ + +// First, correctly encoded ECDSA/Schnorr signatures in data/tx form. +static const valtype sigecdsa = {0x30, 6, 2, 1, 0, 2, 1, 0}; +static const valtype txsigecdsa = {0x30, 6, 2, 1, 0, 2, 1, 0, 0x41}; +static const valtype sigschnorr(64); +static const valtype txsigschnorr = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x41}; + +// An example message to use (9 bytes). +static const valtype msg = {0x73, 0x69, 0x67, 0x63, 0x68, + 0x65, 0x63, 0x6b, 0x73}; + +// A valid pubkey +static const valtype pub = {2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; +// A special key that causes signature checks to return false (see below). +static const valtype badpub = {2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + +// Some small constants with descriptive names to make the purpose clear. +static const valtype nullsig = {}; +static const valtype vfalse = {}; +static const valtype vtrue = {1}; + +static class : public BaseSignatureChecker { + /** + * All null sigs verify false, and all checks using the magic 'bad pubkey' + * value verify false as well. Otherwise, checks verify as true. + */ + bool VerifySignature(const std::vector<uint8_t> &vchSig, + const CPubKey &vchPubKey, + const uint256 &sighash) const final { + if (vchPubKey == CPubKey(badpub)) { + return false; + } + return !vchSig.empty(); + } + + bool CheckSig(const std::vector<uint8_t> &vchSigIn, + const std::vector<uint8_t> &vchPubKey, + const CScript &scriptCode, uint32_t flags) const final { + if (vchPubKey == badpub) { + return false; + } + return !vchSigIn.empty(); + } +} dummysigchecker; + +// construct a 'checkbits' stack element for OP_CHECKMULTISIG (set lower m bits +// to 1, but make sure it's at least n bits long). +static valtype makebits(int m, int n) { + valtype ret(0); + uint64_t bits = (uint64_t(1) << m) - 1; + for (; n > 0; n -= 8, bits >>= 8) { + ret.push_back(uint8_t(bits & 0xff)); + } + return ret; +} + +// Two lists of flag sets to pass to CheckEvalScript. The first is a wider +// set of flags for long-supported opcodes. The latter list is restricted to +// the case with schnorr multisig turned on. +static const std::vector<uint32_t> allflags{ + 0, + SCRIPT_VERIFY_NULLFAIL, + STANDARD_SCRIPT_VERIFY_FLAGS, + STANDARD_SCRIPT_VERIFY_FLAGS | SCRIPT_ENABLE_SCHNORR_MULTISIG, +}; + +static const std::vector<uint32_t> schnorrmultisigflags{ + SCRIPT_VERIFY_NULLFAIL | SCRIPT_ENABLE_SCHNORR_MULTISIG, + STANDARD_SCRIPT_VERIFY_FLAGS | SCRIPT_ENABLE_SCHNORR_MULTISIG, +}; + +static void CheckEvalScript(const stacktype &original_stack, + const CScript &script, + const stacktype &expected_stack, + const int expected_sigchecks, + std::vector<uint32_t> flagset = allflags) { + for (uint32_t flags : flagset) { + ScriptError err = ScriptError::UNKNOWN; + stacktype stack{original_stack}; + ScriptExecutionMetrics metrics; + + bool r = + EvalScript(stack, script, flags, dummysigchecker, metrics, &err); + BOOST_CHECK(r); + BOOST_CHECK_EQUAL(err, ScriptError::OK); + BOOST_CHECK(stack == expected_stack); + BOOST_CHECK_EQUAL(metrics.nSigChecks, expected_sigchecks); + } +} + +BOOST_AUTO_TEST_CASE(test_evalscript) { + CheckEvalScript({}, CScript(), {}, 0); + + CheckEvalScript({nullsig}, CScript() << pub << OP_CHECKSIG, {vfalse}, 0); + CheckEvalScript({txsigecdsa}, CScript() << pub << OP_CHECKSIG, {vtrue}, 1); + CheckEvalScript({txsigschnorr}, CScript() << pub << OP_CHECKSIG, {vtrue}, + 1); + + CheckEvalScript({nullsig}, CScript() << msg << pub << OP_CHECKDATASIG, + {vfalse}, 0); + CheckEvalScript({sigecdsa}, CScript() << msg << pub << OP_CHECKDATASIG, + {vtrue}, 1); + CheckEvalScript({sigschnorr}, CScript() << msg << pub << OP_CHECKDATASIG, + {vtrue}, 1); + + // Check all M-of-N OP_CHECKMULTISIGs combinations in all flavours. + for (int n = 0; n <= MAX_PUBKEYS_PER_MULTISIG; n++) { + for (int m = 0; m <= n; m++) { + // first, generate the spending script + CScript script; + script << m; + for (int i = 0; i < n; i++) { + script << pub; + } + script << n << OP_CHECKMULTISIG; + + stacktype sigs; + + // The all-null-signatures case with null dummy element counts as 0 + // sigchecks, since all signatures are null. + sigs.assign(m + 1, {}); + sigs[0] = {}; + CheckEvalScript(sigs, script, {m ? vfalse : vtrue}, 0); + + // The all-ECDSA-signatures case counts as N sigchecks, except when + // M=0 (so that it counts as 'all-null-signatures" instead). + sigs.assign(m + 1, txsigecdsa); + sigs[0] = {}; + CheckEvalScript(sigs, script, {vtrue}, m ? n : 0); + + // The all-Schnorr-signatures case counts as M sigchecks always. + // (Note that for M=N=0, this actually produces a null dummy which + // executes in legacy mode, but the behaviour is indistinguishable + // from schnorr mode.) + sigs.assign(m + 1, txsigschnorr); + sigs[0] = makebits(m, n); + CheckEvalScript(sigs, script, {vtrue}, m, schnorrmultisigflags); + } + } + + // repeated checks of the same signature count each time + CheckEvalScript({txsigschnorr}, + CScript() << pub << OP_2DUP << OP_CHECKSIGVERIFY + << OP_CHECKSIGVERIFY, + {}, 2); + CheckEvalScript({sigschnorr}, + CScript() << msg << pub << OP_3DUP << OP_CHECKDATASIGVERIFY + << OP_CHECKDATASIGVERIFY, + {}, 2); + + // unexecuted checks (behind if-branches) don't count. + { + CScript script = CScript() << OP_IF << pub << OP_CHECKSIG << OP_ELSE + << OP_DROP << OP_ENDIF; + CheckEvalScript({txsigecdsa, {1}}, script, {vtrue}, 1); + CheckEvalScript({txsigecdsa, {0}}, script, {}, 0); + } + + // Without NULLFAIL, it is possible to have checksig/checkmultisig consume + // CPU using non-null signatures and then return false to the stack, without + // failing. Make sure that this historical case adds sigchecks, so that the + // CPU usage of possible malicious alternate histories (branching off before + // NULLFAIL activated in consensus) can be limited. + CheckEvalScript({txsigecdsa}, CScript() << badpub << OP_CHECKSIG, {vfalse}, + 1, {SCRIPT_VERIFY_NONE}); + CheckEvalScript({{}, txsigecdsa}, + CScript() << 1 << badpub << badpub << badpub << badpub << 4 + << OP_CHECKMULTISIG, + {vfalse}, 4, {SCRIPT_VERIFY_NONE}); + + // CHECKDATASIG and Schnorr need to be checked as well, since they have been + // made retroactively valid since forever and thus alternate histories could + // include them. + CheckEvalScript({sigecdsa}, CScript() << msg << badpub << OP_CHECKDATASIG, + {vfalse}, 1, {SCRIPT_VERIFY_NONE}); + CheckEvalScript({txsigschnorr}, CScript() << badpub << OP_CHECKSIG, + {vfalse}, 1, {SCRIPT_VERIFY_NONE}); + CheckEvalScript({sigschnorr}, CScript() << msg << badpub << OP_CHECKDATASIG, + {vfalse}, 1, {SCRIPT_VERIFY_NONE}); + + // CHECKMULTISIG with schnorr cannot return false, it just fails instead + // (hence, the sigchecks count is unimportant) + { + stacktype stack{{1}, txsigschnorr}; + BOOST_CHECK(!EvalScript( + stack, CScript() << 1 << badpub << 1 << OP_CHECKMULTISIG, + SCRIPT_VERIFY_NONE, dummysigchecker)); + } + { + stacktype stack{{1}, txsigschnorr}; + BOOST_CHECK(!EvalScript( + stack, CScript() << 1 << badpub << 1 << OP_CHECKMULTISIG, + SCRIPT_ENABLE_SCHNORR_MULTISIG, dummysigchecker)); + } + + // EvalScript cumulatively increases the sigchecks count. + { + stacktype stack{txsigschnorr}; + ScriptExecutionMetrics metrics; + metrics.nSigChecks = 12345; + bool r = EvalScript(stack, CScript() << pub << OP_CHECKSIG, + SCRIPT_VERIFY_NONE, dummysigchecker, metrics); + BOOST_CHECK(r); + BOOST_CHECK_EQUAL(metrics.nSigChecks, 12346); + } + + // Other opcodes may be cryptographic and/or CPU intensive, but they do not + // add any additional sigchecks. + static_assert( + (MAX_SCRIPT_SIZE <= 10000 && MAX_OPS_PER_SCRIPT <= 201 && + MAX_STACK_SIZE <= 1000 && MAX_SCRIPT_ELEMENT_SIZE <= 520), + "These can be made far worse with higher limits. Update accordingly."); + + // Hashing operations on the largest stack element. + { + valtype bigblob(MAX_SCRIPT_ELEMENT_SIZE); + CheckEvalScript({}, + CScript() + << bigblob << OP_RIPEMD160 << bigblob << OP_SHA1 + << bigblob << OP_SHA256 << bigblob << OP_HASH160 + << bigblob << OP_HASH256 << OP_CAT << OP_CAT + << OP_CAT << OP_CAT << OP_DROP, + {}, 0); + } + + // OP_ROLL grinding, see + // https://bitslog.com/2017/04/17/new-quadratic-delays-in-bitcoin-scripts/ + { + stacktype bigstack; + bigstack.assign(999, {1}); + CScript script; + for (int i = 0; i < 200; i++) { + script << 998 << OP_ROLL; + } + CheckEvalScript(bigstack, script, bigstack, 0); + } + + // OP_IF grinding, see + // https://bitslog.com/2017/04/17/new-quadratic-delays-in-bitcoin-scripts/ + { + CScript script; + script << 0; + for (int i = 0; i < 100; i++) { + script << OP_IF; + } + for (int i = 0; i < 9798; i++) { + script << 0; + } + for (int i = 0; i < 100; i++) { + script << OP_ENDIF; + } + script << 1; + CheckEvalScript({}, script, {vtrue}, 0); + } + + // OP_CODESEPARATOR grinding, see + // https://gist.github.com/markblundeberg/c2c88d25d5f34213830e48d459cbfb44 + // (this is a simplified form) + { + stacktype stack; + stack.assign(94, txsigecdsa); + CScript script; + for (int i = 0; i < 94; i++) { + script << pub << OP_CHECKSIGVERIFY << OP_CODESEPARATOR; + } + // (remove last codesep) + script.pop_back(); + // Push some garbage to lengthen the script. + valtype bigblob(520); + for (int i = 0; i < 6; i++) { + script << bigblob << bigblob << OP_2DROP; + } + script << 1; + BOOST_CHECK_EQUAL(script.size(), 9666); + CheckEvalScript(stack, script, {vtrue}, 94); + } +} + +BOOST_AUTO_TEST_SUITE_END()