diff --git a/src/script/scriptcache.h b/src/script/scriptcache.h --- a/src/script/scriptcache.h +++ b/src/script/scriptcache.h @@ -5,12 +5,31 @@ #ifndef BITCOIN_SCRIPT_SCRIPTCACHE_H #define BITCOIN_SCRIPT_SCRIPTCACHE_H -#include <uint256.h> - +#include <array> #include <cstdint> class CTransaction; +/** + * The script cache is a map using a key/value element. + * The key is slightly shorter than a power-of-two size to make room for + * the value. + */ +class ScriptCacheKey { + std::array<uint8_t, 30> data; + +public: + ScriptCacheKey() = default; + ScriptCacheKey(const ScriptCacheKey &rhs) = default; + ScriptCacheKey(const CTransaction &tx, uint32_t flags); + + bool operator==(const ScriptCacheKey &rhs) const { + return rhs.data == data; + } + + friend class ScriptCacheHasher; +}; + // DoS prevention: limit cache size to 32MB (over 1000000 entries on 64-bit // systems). Due to how we count cache size, actual memory usage is slightly // more (~32.25 MB) @@ -21,13 +40,18 @@ /** Initializes the script-execution cache */ void InitScriptExecutionCache(); -/** Compute the cache key for a given transaction and flags. */ -uint256 GetScriptCacheKey(const CTransaction &tx, uint32_t flags); +/** Check if a given key is in the cache, and if so, return its values. */ +bool IsKeyInScriptCache(ScriptCacheKey key, bool erase, int &nSigChecksRet); -/** Check if a given key is in the cache. */ -bool IsKeyInScriptCache(uint256 key, bool erase); +constexpr int SCRIPT_CACHE_MAX_SIGCHECKS = UINT16_MAX; -/** Add an entry in the cache. */ -void AddKeyInScriptCache(uint256 key); +/** + * Add an entry in the cache. The sigchecks count must be in the range + * 0-SCRIPT_CACHE_MAX_SIGCHECKS inclusive, otherwise a cache entry must not + * be added (if successfully added, it would be corrupt data, and possibly + * lead to consensus failure when the cache is consulted during block + * validation). + */ +void AddKeyInScriptCache(ScriptCacheKey key, int nSigChecks); #endif // BITCOIN_SCRIPT_SCRIPTCACHE_H diff --git a/src/script/scriptcache.cpp b/src/script/scriptcache.cpp --- a/src/script/scriptcache.cpp +++ b/src/script/scriptcache.cpp @@ -10,10 +10,71 @@ #include <random.h> #include <script/sigcache.h> #include <sync.h> +#include <uint256.h> #include <util/system.h> #include <validation.h> -static CuckooCache::cache<CuckooCache::KeyOnly<uint256>, SignatureCacheHasher> +#include <limits> + +/** + * In future if many more values are added, it should be considered to + * expand the element size to 64 bytes (with padding the spare space as + * needed) so the key can be long. Shortening the key too much risks + * opening ourselves up to consensus-failing collisions, however it should + * be noted that our cache nonce is private and unique, so collisions would + * affect only one node and attackers have no way of offline-preparing a + * collision attack even on short keys. + */ +struct ScriptCacheElement { + using KeyType = ScriptCacheKey; + + KeyType key; + uint16_t nSigChecksCount; + + static_assert(SCRIPT_CACHE_MAX_SIGCHECKS == + std::numeric_limits<decltype(nSigChecksCount)>::max(), + "the declared maximum must match the type"); + + ScriptCacheElement() = default; + ScriptCacheElement(const ScriptCacheElement &rhs) = default; + + ScriptCacheElement(const KeyType &keyIn, uint16_t nSigChecksIn) + : key(keyIn), nSigChecksCount(nSigChecksIn) {} + + const KeyType &getKey() const { return key; } +}; + +static_assert(sizeof(ScriptCacheElement) == 32, + "ScriptCacheElement must be 32 bytes"); + +class ScriptCacheHasher { +public: + template <uint8_t hash_select> + uint32_t operator()(const ScriptCacheKey &k) const { + static_assert(hash_select < 8, "only has 8 hashes available."); + + const auto &d = k.data; + + static_assert(sizeof(d) == 30, + "modify the following if key size changes"); + + uint32_t u; + if (hash_select < 7) { + std::memcpy(&u, d.data() + 4 * hash_select, 4); + } else { + // We are required to produce 8 subhashes. We have only two + // key bytes left over but this is fine. We put the leftovers + // on the higher bits, as these are what primarily get used to + // determine the index. The lower bits are filled by reusing + // some other bytes. + u = (uint32_t(d[28]) << 24) + (uint32_t(d[29]) << 16) + + (uint32_t(d[3]) << 8) + uint32_t(d[7]); + } + return u; + } +}; + +static CuckooCache::cache<ScriptCacheElement, ScriptCacheHasher> scriptExecutionCache; static uint256 scriptExecutionCacheNonce(GetRandHash()); @@ -32,8 +93,8 @@ (nElems * sizeof(uint256)) >> 20, nMaxCacheSize >> 20, nElems); } -uint256 GetScriptCacheKey(const CTransaction &tx, uint32_t flags) { - uint256 key; +ScriptCacheKey::ScriptCacheKey(const CTransaction &tx, uint32_t flags) { + uint256 hash; // We only use the first 19 bytes of nonce to avoid a second SHA round - // giving us 19 + 32 + 4 = 55 bytes (+ 8 + 1 = 64) static_assert(55 - sizeof(flags) - 32 >= 128 / 8, @@ -42,21 +103,32 @@ .Write(scriptExecutionCacheNonce.begin(), 55 - sizeof(flags) - 32) .Write(tx.GetHash().begin(), 32) .Write((uint8_t *)&flags, sizeof(flags)) - .Finalize(key.begin()); + .Finalize(hash.begin()); - return key; + assert(data.size() < hash.size()); + std::copy(hash.begin() + hash.size() - data.size(), hash.end(), + data.begin()); } -bool IsKeyInScriptCache(uint256 key, bool erase) { +bool IsKeyInScriptCache(ScriptCacheKey key, bool erase, int &nSigChecksRet) { // TODO: Remove this requirement by making CuckooCache not require external // locks AssertLockHeld(cs_main); - return scriptExecutionCache.contains(key, erase); + ScriptCacheElement elem(key, 0); + bool ret = scriptExecutionCache.get(elem, erase); + if (ret) { + nSigChecksRet = elem.nSigChecksCount; + } + return ret; } -void AddKeyInScriptCache(uint256 key) { +void AddKeyInScriptCache(ScriptCacheKey key, int nSigChecks) { + if (0 > nSigChecks || nSigChecks > SCRIPT_CACHE_MAX_SIGCHECKS) { + throw std::range_error("sigchecks out of range"); + } // TODO: Remove this requirement by making CuckooCache not require external // locks AssertLockHeld(cs_main); - scriptExecutionCache.insert(key); + ScriptCacheElement elem(key, nSigChecks); + scriptExecutionCache.insert(elem); } diff --git a/src/test/txvalidationcache_tests.cpp b/src/test/txvalidationcache_tests.cpp --- a/src/test/txvalidationcache_tests.cpp +++ b/src/test/txvalidationcache_tests.cpp @@ -431,4 +431,71 @@ } } +BOOST_AUTO_TEST_CASE(scriptcache_values) { + // Test insertion and querying of keys&values from the script cache. + + InitScriptExecutionCache(); + + // construct four distinct keys from very slightly different data + CMutableTransaction tx1; + tx1.nVersion = 1; + CMutableTransaction tx2; + tx2.nVersion = 2; + uint32_t flagsA = 0x7fffffff; + uint32_t flagsB = 0xffffffff; + ScriptCacheKey key1A(CTransaction(tx1), flagsA); + ScriptCacheKey key1B(CTransaction(tx1), flagsB); + ScriptCacheKey key2A(CTransaction(tx2), flagsA); + ScriptCacheKey key2B(CTransaction(tx2), flagsB); + + BOOST_CHECK(key1A == key1A); + BOOST_CHECK(!(key1A == key1B)); + BOOST_CHECK(!(key1A == key2A)); + BOOST_CHECK(!(key1A == key2B)); + BOOST_CHECK(key1B == key1B); + BOOST_CHECK(!(key1B == key2A)); + BOOST_CHECK(!(key1B == key2B)); + BOOST_CHECK(key2A == key2A); + BOOST_CHECK(!(key2A == key2B)); + BOOST_CHECK(key2B == key2B); + + int nSigChecks = 0xfeed; + + // key is not yet inserted + BOOST_CHECK(!IsKeyInScriptCache(key1A, false, nSigChecks)); + // add the key and check it worked + AddKeyInScriptCache(key1A, 42); + BOOST_CHECK(IsKeyInScriptCache(key1A, false, nSigChecks)); + BOOST_CHECK(nSigChecks == 42); + + BOOST_CHECK(!IsKeyInScriptCache(key1B, false, nSigChecks)); + BOOST_CHECK(!IsKeyInScriptCache(key2A, false, nSigChecks)); + BOOST_CHECK(!IsKeyInScriptCache(key2B, false, nSigChecks)); + + // add a couple more keys with value the limit of storeable range + AddKeyInScriptCache(key1B, 0); + AddKeyInScriptCache(key2A, 0xffff); + + // try adding an unstoreable value + BOOST_CHECK_THROW(AddKeyInScriptCache(key2B, 0x10000), std::range_error); + BOOST_CHECK_THROW(AddKeyInScriptCache(key2B, -1), std::range_error); + + // read out values again + nSigChecks = -1; + BOOST_CHECK(IsKeyInScriptCache(key1A, false, nSigChecks)); + BOOST_CHECK(nSigChecks == 42); + BOOST_CHECK(IsKeyInScriptCache(key1B, false, nSigChecks)); + BOOST_CHECK(nSigChecks == 0); + BOOST_CHECK(IsKeyInScriptCache(key2A, false, nSigChecks)); + BOOST_CHECK(nSigChecks == 0xffff); + BOOST_CHECK(!IsKeyInScriptCache(key2B, false, nSigChecks)); + + // try overwriting an existing entry with different value (should never + // happen in practice but see what happens) + AddKeyInScriptCache(key1A, 99); + BOOST_CHECK(IsKeyInScriptCache(key1A, false, nSigChecks)); + // currently, no replacement is done + BOOST_CHECK(nSigChecks == 42); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1232,8 +1232,10 @@ // Note that this assumes that the inputs provided are correct (ie that the // transaction hash which is in tx's prevouts properly commits to the // scriptPubKey in the inputs view of that transaction). - uint256 hashCacheEntry = GetScriptCacheKey(tx, flags); - if (IsKeyInScriptCache(hashCacheEntry, !scriptCacheStore)) { + int nSigChecksDummy; + ScriptCacheKey hashCacheEntry(tx, flags); + if (IsKeyInScriptCache(hashCacheEntry, !scriptCacheStore, + nSigChecksDummy)) { return true; } @@ -1296,7 +1298,7 @@ if (scriptCacheStore && !pvChecks) { // We executed all of the provided scripts, and were told to cache the // result. Do so now. - AddKeyInScriptCache(hashCacheEntry); + AddKeyInScriptCache(hashCacheEntry, 0); } return true;