Skip to content

Commit b5fea8d

Browse files
committed
Cache full script execution results in addition to signatures
This adds a new CuckooCache in validation, caching whether all of a transaction's scripts were valid with a given set of script flags. Unlike previous attempts at caching an entire transaction's validity, which have nearly universally introduced consensus failures, this only caches the validity of a transaction's scriptSigs. As these are pure functions of the transaction and data it commits to, this should be much safer. This is somewhat duplicative with the sigcache, as entries in the new cache will also have several entries in the sigcache. However, the sigcache is kept both as ATMP relies on it and because it prevents malleability-based DoS attacks on the new higher-level cache. Instead, the -sigcachesize option is re-used - cutting the sigcache size in half and using the newly freed memory for the script execution cache. Transactions which match the script execution cache never even have entries in the script check thread's workqueue created. Note that the cache is indexed only on the script execution flags and the transaction's witness hash. While this is sufficient to make the CScriptCheck() calls pure functions, this introduces dependancies on the mempool calculating things such as the PrecomputedTransactionData object, filling the CCoinsViewCache, etc in the exact same way as ConnectBlock. I belive this is a reasonable assumption, but should be noted carefully. In a rather naive benchmark (reindex-chainstate up to block 284k with cuckoocache always returning true for contains(), -assumevalid=0 and a very large dbcache), this connected blocks ~1.7x faster.
1 parent 6d22b2b commit b5fea8d

File tree

5 files changed

+61
-14
lines changed

5 files changed

+61
-14
lines changed

src/init.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1211,6 +1211,7 @@ bool AppInitMain(boost::thread_group& threadGroup, CScheduler& scheduler)
12111211
LogPrintf("Using at most %i automatic connections (%i file descriptors available)\n", nMaxConnections, nFD);
12121212

12131213
InitSignatureCache();
1214+
InitScriptExecutionCache();
12141215

12151216
LogPrintf("Using %u threads for script verification\n", nScriptCheckThreads);
12161217
if (nScriptCheckThreads) {

src/script/sigcache.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ void InitSignatureCache()
7474
{
7575
// nMaxCacheSize is unsigned. If -maxsigcachesize is set to zero,
7676
// setup_bytes creates the minimum possible cache (2 elements).
77-
size_t nMaxCacheSize = std::min(std::max((int64_t)0, GetArg("-maxsigcachesize", DEFAULT_MAX_SIG_CACHE_SIZE)), MAX_MAX_SIG_CACHE_SIZE) * ((size_t) 1 << 20);
77+
size_t nMaxCacheSize = std::min(std::max((int64_t)0, GetArg("-maxsigcachesize", DEFAULT_MAX_SIG_CACHE_SIZE) / 2), MAX_MAX_SIG_CACHE_SIZE) * ((size_t) 1 << 20);
7878
size_t nElems = signatureCache.setup_bytes(nMaxCacheSize);
7979
LogPrintf("Using %zu MiB out of %zu requested for signature cache, able to store %zu elements\n",
8080
(nElems*sizeof(uint256)) >>20, nMaxCacheSize>>20, nElems);

src/test/test_bitcoin.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ BasicTestingSetup::BasicTestingSetup(const std::string& chainName)
3939
SetupEnvironment();
4040
SetupNetworking();
4141
InitSignatureCache();
42+
InitScriptExecutionCache();
4243
fPrintToDebugLog = false; // don't want to write to debug.log file
4344
fCheckBlockIndex = true;
4445
SelectParams(chainName);

src/validation.cpp

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "consensus/merkle.h"
1515
#include "consensus/tx_verify.h"
1616
#include "consensus/validation.h"
17+
#include "cuckoocache.h"
1718
#include "fs.h"
1819
#include "hash.h"
1920
#include "init.h"
@@ -189,7 +190,7 @@ enum FlushStateMode {
189190
static bool FlushStateToDisk(const CChainParams& chainParams, CValidationState &state, FlushStateMode mode, int nManualPruneHeight=0);
190191
static void FindFilesToPruneManual(std::set<int>& setFilesToPrune, int nManualPruneHeight);
191192
static void FindFilesToPrune(std::set<int>& setFilesToPrune, uint64_t nPruneAfterHeight);
192-
static bool CheckInputs(const CTransaction& tx, CValidationState &state, const CCoinsViewCache &inputs, bool fScriptChecks, unsigned int flags, bool cacheStore, PrecomputedTransactionData& txdata, std::vector<CScriptCheck> *pvChecks = NULL);
193+
static bool CheckInputs(const CTransaction& tx, CValidationState &state, const CCoinsViewCache &inputs, bool fScriptChecks, unsigned int flags, bool cacheSigStore, bool cacheFullScriptStore, PrecomputedTransactionData& txdata, std::vector<CScriptCheck> *pvChecks = nullptr);
193194
static FILE* OpenUndoFile(const CDiskBlockPos &pos, bool fReadOnly = false);
194195

195196
bool CheckFinalTx(const CTransaction &tx, int flags)
@@ -752,29 +753,36 @@ static bool AcceptToMemoryPoolWorker(const CChainParams& chainparams, CTxMemPool
752753
// Check against previous transactions
753754
// This is done last to help prevent CPU exhaustion denial-of-service attacks.
754755
PrecomputedTransactionData txdata(tx);
755-
if (!CheckInputs(tx, state, view, true, scriptVerifyFlags, true, txdata)) {
756+
if (!CheckInputs(tx, state, view, true, scriptVerifyFlags, true, false, txdata)) {
756757
// SCRIPT_VERIFY_CLEANSTACK requires SCRIPT_VERIFY_WITNESS, so we
757758
// need to turn both off, and compare against just turning off CLEANSTACK
758759
// to see if the failure is specifically due to witness validation.
759760
CValidationState stateDummy; // Want reported failures to be from first CheckInputs
760-
if (!tx.HasWitness() && CheckInputs(tx, stateDummy, view, true, scriptVerifyFlags & ~(SCRIPT_VERIFY_WITNESS | SCRIPT_VERIFY_CLEANSTACK), true, txdata) &&
761-
!CheckInputs(tx, stateDummy, view, true, scriptVerifyFlags & ~SCRIPT_VERIFY_CLEANSTACK, true, txdata)) {
761+
if (!tx.HasWitness() && CheckInputs(tx, stateDummy, view, true, scriptVerifyFlags & ~(SCRIPT_VERIFY_WITNESS | SCRIPT_VERIFY_CLEANSTACK), true, false, txdata) &&
762+
!CheckInputs(tx, stateDummy, view, true, scriptVerifyFlags & ~SCRIPT_VERIFY_CLEANSTACK, true, false, txdata)) {
762763
// Only the witness is missing, so the transaction itself may be fine.
763764
state.SetCorruptionPossible();
764765
}
765766
return false; // state filled in by CheckInputs
766767
}
767768

768-
// Check again against just the consensus-critical mandatory script
769-
// verification flags, in case of bugs in the standard flags that cause
769+
// Check again against the current block tip's script verification
770+
// flags to cache our script execution flags. This is, of course,
771+
// useless if the next block has different script flags from the
772+
// previous one, but because the cache tracks script flags for us it
773+
// will auto-invalidate and we'll just have a few blocks of extra
774+
// misses on soft-fork activation.
775+
//
776+
// This is also useful in case of bugs in the standard flags that cause
770777
// transactions to pass as valid when they're actually invalid. For
771778
// instance the STRICTENC flag was incorrectly allowing certain
772779
// CHECKSIG NOT scripts to pass, even though they were invalid.
773780
//
774781
// There is a similar check in CreateNewBlock() to prevent creating
775-
// invalid blocks, however allowing such transactions into the mempool
776-
// can be exploited as a DoS attack.
777-
if (!CheckInputs(tx, state, view, true, MANDATORY_SCRIPT_VERIFY_FLAGS, true, txdata))
782+
// invalid blocks (using TestBlockValidity), however allowing such
783+
// transactions into the mempool can be exploited as a DoS attack.
784+
unsigned int currentBlockScriptVerifyFlags = GetBlockScriptFlags(chainActive.Tip(), Params().GetConsensus());
785+
if (!CheckInputs(tx, state, view, true, currentBlockScriptVerifyFlags, true, true, txdata))
778786
{
779787
return error("%s: BUG! PLEASE REPORT THIS! ConnectInputs failed against MANDATORY but not STANDARD flags %s, %s",
780788
__func__, hash.ToString(), FormatStateMessage(state));
@@ -1152,12 +1160,25 @@ int GetSpendHeight(const CCoinsViewCache& inputs)
11521160
return pindexPrev->nHeight + 1;
11531161
}
11541162

1163+
1164+
static CuckooCache::cache<uint256, SignatureCacheHasher> scriptExecutionCache;
1165+
static uint256 scriptExecutionCacheNonce(GetRandHash());
1166+
1167+
void InitScriptExecutionCache() {
1168+
// nMaxCacheSize is unsigned. If -maxsigcachesize is set to zero,
1169+
// setup_bytes creates the minimum possible cache (2 elements).
1170+
size_t nMaxCacheSize = std::min(std::max((int64_t)0, GetArg("-maxsigcachesize", DEFAULT_MAX_SIG_CACHE_SIZE) / 2), MAX_MAX_SIG_CACHE_SIZE) * ((size_t) 1 << 20);
1171+
size_t nElems = scriptExecutionCache.setup_bytes(nMaxCacheSize);
1172+
LogPrintf("Using %zu MiB out of %zu requested for script execution cache, able to store %zu elements\n",
1173+
(nElems*sizeof(uint256)) >>20, nMaxCacheSize>>20, nElems);
1174+
}
1175+
11551176
/**
11561177
* Check whether all inputs of this transaction are valid (no double spends, scripts & sigs, amounts)
11571178
* This does not modify the UTXO set. If pvChecks is not NULL, script checks are pushed onto it
11581179
* instead of being performed inline.
11591180
*/
1160-
static bool CheckInputs(const CTransaction& tx, CValidationState &state, const CCoinsViewCache &inputs, bool fScriptChecks, unsigned int flags, bool cacheStore, PrecomputedTransactionData& txdata, std::vector<CScriptCheck> *pvChecks)
1181+
static bool CheckInputs(const CTransaction& tx, CValidationState &state, const CCoinsViewCache &inputs, bool fScriptChecks, unsigned int flags, bool cacheSigStore, bool cacheFullScriptStore, PrecomputedTransactionData& txdata, std::vector<CScriptCheck> *pvChecks)
11611182
{
11621183
if (!tx.IsCoinBase())
11631184
{
@@ -1177,6 +1198,21 @@ static bool CheckInputs(const CTransaction& tx, CValidationState &state, const C
11771198
// Of course, if an assumed valid block is invalid due to false scriptSigs
11781199
// this optimization would allow an invalid chain to be accepted.
11791200
if (fScriptChecks) {
1201+
// First check if script executions have been cached with the same
1202+
// flags. Note that this assumes that the inputs provided are
1203+
// correct (ie that the transaction hash which is in tx's prevouts
1204+
// properly commits to the scriptPubKey in the inputs view of that
1205+
// transaction).
1206+
uint256 hashCacheEntry;
1207+
// We only use the first 19 bytes of nonce to avoid a second SHA
1208+
// round - giving us 19 + 32 + 4 = 55 bytes (+ 8 + 1 = 64)
1209+
static_assert(55 - sizeof(flags) - 32 >= 128/8, "Want at least 128 bits of nonce for script execution cache");
1210+
CSHA256().Write(scriptExecutionCacheNonce.begin(), 55 - sizeof(flags) - 32).Write(tx.GetWitnessHash().begin(), 32).Write((unsigned char*)&flags, sizeof(flags)).Finalize(hashCacheEntry.begin());
1211+
AssertLockHeld(cs_main); //TODO: Remove this requirement by making CuckooCache not require external locks
1212+
if (scriptExecutionCache.contains(hashCacheEntry, !cacheFullScriptStore)) {
1213+
return true;
1214+
}
1215+
11801216
for (unsigned int i = 0; i < tx.vin.size(); i++) {
11811217
const COutPoint &prevout = tx.vin[i].prevout;
11821218
const Coin& coin = inputs.AccessCoin(prevout);
@@ -1191,7 +1227,7 @@ static bool CheckInputs(const CTransaction& tx, CValidationState &state, const C
11911227
const CAmount amount = coin.out.nValue;
11921228

11931229
// Verify signature
1194-
CScriptCheck check(scriptPubKey, amount, tx, i, flags, cacheStore, &txdata);
1230+
CScriptCheck check(scriptPubKey, amount, tx, i, flags, cacheSigStore, &txdata);
11951231
if (pvChecks) {
11961232
pvChecks->push_back(CScriptCheck());
11971233
check.swap(pvChecks->back());
@@ -1204,7 +1240,7 @@ static bool CheckInputs(const CTransaction& tx, CValidationState &state, const C
12041240
// avoid splitting the network between upgraded and
12051241
// non-upgraded nodes.
12061242
CScriptCheck check2(scriptPubKey, amount, tx, i,
1207-
flags & ~STANDARD_NOT_MANDATORY_VERIFY_FLAGS, cacheStore, &txdata);
1243+
flags & ~STANDARD_NOT_MANDATORY_VERIFY_FLAGS, cacheSigStore, &txdata);
12081244
if (check2())
12091245
return state.Invalid(false, REJECT_NONSTANDARD, strprintf("non-mandatory-script-verify-flag (%s)", ScriptErrorString(check.GetScriptError())));
12101246
}
@@ -1218,6 +1254,12 @@ static bool CheckInputs(const CTransaction& tx, CValidationState &state, const C
12181254
return state.DoS(100,false, REJECT_INVALID, strprintf("mandatory-script-verify-flag-failed (%s)", ScriptErrorString(check.GetScriptError())));
12191255
}
12201256
}
1257+
1258+
if (cacheFullScriptStore && !pvChecks) {
1259+
// We executed all of the provided scripts, and were told to
1260+
// cache the result. Do so now.
1261+
scriptExecutionCache.insert(hashCacheEntry);
1262+
}
12211263
}
12221264
}
12231265

@@ -1684,7 +1726,7 @@ static bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockInd
16841726

16851727
std::vector<CScriptCheck> vChecks;
16861728
bool fCacheResults = fJustCheck; /* Don't cache results if we're actually connecting blocks (still consult the cache, though) */
1687-
if (!CheckInputs(tx, state, view, fScriptChecks, flags, fCacheResults, txdata[i], nScriptCheckThreads ? &vChecks : NULL))
1729+
if (!CheckInputs(tx, state, view, fScriptChecks, flags, fCacheResults, fCacheResults, txdata[i], nScriptCheckThreads ? &vChecks : NULL))
16881730
return error("ConnectBlock(): CheckInputs on %s failed with %s",
16891731
tx.GetHash().ToString(), FormatStateMessage(state));
16901732
control.Add(vChecks);

src/validation.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,9 @@ class CScriptCheck
394394
ScriptError GetScriptError() const { return error; }
395395
};
396396

397+
/** Initializes the script-execution cache */
398+
void InitScriptExecutionCache();
399+
397400

398401
/** Functions for disk access for blocks */
399402
bool ReadBlockFromDisk(CBlock& block, const CDiskBlockPos& pos, const Consensus::Params& consensusParams);

0 commit comments

Comments
 (0)