Skip to content

Commit b50cd7a

Browse files
committed
Fix dangerous condition in ModifyNewCoins.
We were marking coins FRESH before being sure they were not overwriting dirty undo data. This condition was never reached in existing code because undo data was always flushed before UpdateCoins was called with new transactions, but could have been exposed in an otherwise safe refactor. Clarify in the comments the assumptions made in ModifyNewCoins. Add ability to undo transactions to UpdateCoins unit test. Thanks to Russ Yanofsky for suggestion on how to make logic clearer and fixing up the ccoins_modify_new test cases.
1 parent caa2f10 commit b50cd7a

File tree

4 files changed

+181
-62
lines changed

4 files changed

+181
-62
lines changed

src/coins.cpp

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,37 @@ CCoinsModifier CCoinsViewCache::ModifyCoins(const uint256 &txid) {
117117
return CCoinsModifier(*this, ret.first, cachedCoinUsage);
118118
}
119119

120-
// ModifyNewCoins has to know whether the new outputs its creating are for a
121-
// coinbase or not. If they are for a coinbase, it can not mark them as fresh.
122-
// This is to ensure that the historical duplicate coinbases before BIP30 was
123-
// in effect will still be properly overwritten when spent.
120+
/* ModifyNewCoins allows for faster coin modification when creating the new
121+
* outputs from a transaction. It assumes that BIP 30 (no duplicate txids)
122+
* applies and has already been tested for (or the test is not required due to
123+
* BIP 34, height in coinbase). If we can assume BIP 30 then we know that any
124+
* non-coinbase transaction we are adding to the UTXO must not already exist in
125+
* the utxo unless it is fully spent. Thus we can check only if it exists DIRTY
126+
* at the current level of the cache, in which case it is not safe to mark it
127+
* FRESH (b/c then its spentness still needs to flushed). If it's not dirty and
128+
* doesn't exist or is pruned in the current cache, we know it either doesn't
129+
* exist or is pruned in parent caches, which is the definition of FRESH. The
130+
* exception to this is the two historical violations of BIP 30 in the chain,
131+
* both of which were coinbases. We do not mark these fresh so we we can ensure
132+
* that they will still be properly overwritten when spent.
133+
*/
124134
CCoinsModifier CCoinsViewCache::ModifyNewCoins(const uint256 &txid, bool coinbase) {
125135
assert(!hasModifier);
126136
std::pair<CCoinsMap::iterator, bool> ret = cacheCoins.insert(std::make_pair(txid, CCoinsCacheEntry()));
127-
ret.first->second.coins.Clear();
128137
if (!coinbase) {
129-
ret.first->second.flags = CCoinsCacheEntry::FRESH;
138+
// New coins must not already exist.
139+
if (!ret.first->second.coins.IsPruned())
140+
throw std::logic_error("ModifyNewCoins should not find pre-existing coins on a non-coinbase unless they are pruned!");
141+
142+
if (!(ret.first->second.flags & CCoinsCacheEntry::DIRTY)) {
143+
// If the coin is known to be pruned (have no unspent outputs) in
144+
// the current view and the cache entry is not dirty, we know the
145+
// coin also must be pruned in the parent view as well, so it is safe
146+
// to mark this fresh.
147+
ret.first->second.flags |= CCoinsCacheEntry::FRESH;
148+
}
130149
}
150+
ret.first->second.coins.Clear();
131151
ret.first->second.flags |= CCoinsCacheEntry::DIRTY;
132152
return CCoinsModifier(*this, ret.first, 0);
133153
}
@@ -200,6 +220,11 @@ bool CCoinsViewCache::BatchWrite(CCoinsMap &mapCoins, const uint256 &hashBlockIn
200220
itUs->second.coins.swap(it->second.coins);
201221
cachedCoinsUsage += itUs->second.coins.DynamicMemoryUsage();
202222
itUs->second.flags |= CCoinsCacheEntry::DIRTY;
223+
// NOTE: It is possible the child has a FRESH flag here in
224+
// the event the entry we found in the parent is pruned. But
225+
// we must not copy that FRESH flag to the parent as that
226+
// pruned state likely still needs to be communicated to the
227+
// grandparent.
203228
}
204229
}
205230
}

src/coins.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,11 @@ struct CCoinsCacheEntry
269269
enum Flags {
270270
DIRTY = (1 << 0), // This cache entry is potentially different from the version in the parent view.
271271
FRESH = (1 << 1), // The parent view does not have this entry (or it is pruned).
272+
/* Note that FRESH is a performance optimization with which we can
273+
* erase coins that are fully spent if we know we do not need to
274+
* flush the changes to the parent cache. It is always safe to
275+
* not mark FRESH if that condition is not guaranteed.
276+
*/
272277
};
273278

274279
CCoinsCacheEntry() : coins(), flags(0) {}

src/test/coins_tests.cpp

Lines changed: 144 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "coins.h"
66
#include "script/standard.h"
77
#include "uint256.h"
8+
#include "undo.h"
89
#include "utilstrencodings.h"
910
#include "test/test_bitcoin.h"
1011
#include "test/test_random.h"
@@ -16,6 +17,9 @@
1617

1718
#include <boost/test/unit_test.hpp>
1819

20+
bool ApplyTxInUndo(const CTxInUndo& undo, CCoinsViewCache& view, const COutPoint& out);
21+
void UpdateCoins(const CTransaction& tx, CCoinsViewCache& inputs, CTxUndo &txundo, int nHeight);
22+
1923
namespace
2024
{
2125
class CCoinsViewTest : public CCoinsView
@@ -213,6 +217,22 @@ BOOST_AUTO_TEST_CASE(coins_cache_simulation_test)
213217
BOOST_CHECK(missed_an_entry);
214218
}
215219

220+
typedef std::tuple<CTransaction,CTxUndo,CCoins> TxData;
221+
// Store of all necessary tx and undo data for next test
222+
std::map<uint256, TxData> alltxs;
223+
224+
TxData &FindRandomFrom(const std::set<uint256> &txidset) {
225+
assert(txidset.size());
226+
std::set<uint256>::iterator txIt = txidset.lower_bound(GetRandHash());
227+
if (txIt == txidset.end()) {
228+
txIt = txidset.begin();
229+
}
230+
std::map<uint256, TxData>::iterator txdit = alltxs.find(*txIt);
231+
assert(txdit != alltxs.end());
232+
return txdit->second;
233+
}
234+
235+
216236
// This test is similar to the previous test
217237
// except the emphasis is on testing the functionality of UpdateCoins
218238
// random txs are created and UpdateCoins is used to update the cache stack
@@ -229,77 +249,139 @@ BOOST_AUTO_TEST_CASE(updatecoins_simulation_test)
229249
std::vector<CCoinsViewCacheTest*> stack; // A stack of CCoinsViewCaches on top.
230250
stack.push_back(new CCoinsViewCacheTest(&base)); // Start with one cache.
231251

232-
// Track the txids we've used and whether they have been spent or not
233-
std::map<uint256, CAmount> coinbaseids;
234-
std::set<uint256> alltxids;
252+
// Track the txids we've used in various sets
253+
std::set<uint256> coinbaseids;
254+
std::set<uint256> disconnectedids;
235255
std::set<uint256> duplicateids;
256+
std::set<uint256> utxoset;
236257

237258
for (unsigned int i = 0; i < NUM_SIMULATION_ITERATIONS; i++) {
238-
{
259+
uint32_t randiter = insecure_rand();
260+
261+
// 19/20 txs add a new transaction
262+
if (randiter % 20 < 19) {
239263
CMutableTransaction tx;
240264
tx.vin.resize(1);
241265
tx.vout.resize(1);
242266
tx.vout[0].nValue = i; //Keep txs unique unless intended to duplicate
243267
unsigned int height = insecure_rand();
268+
CCoins oldcoins;
244269

245-
// 1/10 times create a coinbase
246-
if (insecure_rand() % 10 == 0 || coinbaseids.size() < 10) {
247-
// 1/100 times create a duplicate coinbase
270+
// 2/20 times create a new coinbase
271+
if (randiter % 20 < 2 || coinbaseids.size() < 10) {
272+
// 1/10 of those times create a duplicate coinbase
248273
if (insecure_rand() % 10 == 0 && coinbaseids.size()) {
249-
std::map<uint256, CAmount>::iterator coinbaseIt = coinbaseids.lower_bound(GetRandHash());
250-
if (coinbaseIt == coinbaseids.end()) {
251-
coinbaseIt = coinbaseids.begin();
252-
}
253-
//Use same random value to have same hash and be a true duplicate
254-
tx.vout[0].nValue = coinbaseIt->second;
255-
assert(tx.GetHash() == coinbaseIt->first);
256-
duplicateids.insert(coinbaseIt->first);
274+
TxData &txd = FindRandomFrom(coinbaseids);
275+
// Reuse the exact same coinbase
276+
tx = std::get<0>(txd);
277+
// shouldn't be available for reconnection if its been duplicated
278+
disconnectedids.erase(tx.GetHash());
279+
280+
duplicateids.insert(tx.GetHash());
257281
}
258282
else {
259-
coinbaseids[tx.GetHash()] = tx.vout[0].nValue;
283+
coinbaseids.insert(tx.GetHash());
260284
}
261285
assert(CTransaction(tx).IsCoinBase());
262286
}
263-
// 9/10 times create a regular tx
287+
288+
// 17/20 times reconnect previous or add a regular tx
264289
else {
290+
265291
uint256 prevouthash;
266-
// equally likely to spend coinbase or non coinbase
267-
std::set<uint256>::iterator txIt = alltxids.lower_bound(GetRandHash());
268-
if (txIt == alltxids.end()) {
269-
txIt = alltxids.begin();
292+
// 1/20 times reconnect a previously disconnected tx
293+
if (randiter % 20 == 2 && disconnectedids.size()) {
294+
TxData &txd = FindRandomFrom(disconnectedids);
295+
tx = std::get<0>(txd);
296+
prevouthash = tx.vin[0].prevout.hash;
297+
if (!CTransaction(tx).IsCoinBase() && !utxoset.count(prevouthash)) {
298+
disconnectedids.erase(tx.GetHash());
299+
continue;
300+
}
301+
302+
// If this tx is already IN the UTXO, then it must be a coinbase, and it must be a duplicate
303+
if (utxoset.count(tx.GetHash())) {
304+
assert(CTransaction(tx).IsCoinBase());
305+
assert(duplicateids.count(tx.GetHash()));
306+
}
307+
disconnectedids.erase(tx.GetHash());
270308
}
271-
prevouthash = *txIt;
272309

273-
// Construct the tx to spend the coins of prevouthash
274-
tx.vin[0].prevout.hash = prevouthash;
275-
tx.vin[0].prevout.n = 0;
310+
// 16/20 times create a regular tx
311+
else {
312+
TxData &txd = FindRandomFrom(utxoset);
313+
prevouthash = std::get<0>(txd).GetHash();
276314

315+
// Construct the tx to spend the coins of prevouthash
316+
tx.vin[0].prevout.hash = prevouthash;
317+
tx.vin[0].prevout.n = 0;
318+
assert(!CTransaction(tx).IsCoinBase());
319+
}
320+
// In this simple test coins only have two states, spent or unspent, save the unspent state to restore
321+
oldcoins = result[prevouthash];
277322
// Update the expected result of prevouthash to know these coins are spent
278-
CCoins& oldcoins = result[prevouthash];
279-
oldcoins.Clear();
323+
result[prevouthash].Clear();
280324

281-
// It is of particular importance here that once we spend a coinbase tx hash
282-
// it is no longer available to be duplicated (or spent again)
283-
// BIP 34 in conjunction with enforcing BIP 30 (at least until BIP 34 was active)
284-
// results in the fact that no coinbases were duplicated after they were already spent
285-
alltxids.erase(prevouthash);
286-
coinbaseids.erase(prevouthash);
325+
utxoset.erase(prevouthash);
287326

288327
// The test is designed to ensure spending a duplicate coinbase will work properly
289328
// if that ever happens and not resurrect the previously overwritten coinbase
290329
if (duplicateids.count(prevouthash))
291330
spent_a_duplicate_coinbase = true;
292331

293-
assert(!CTransaction(tx).IsCoinBase());
294332
}
295-
// Track this tx to possibly spend later
296-
alltxids.insert(tx.GetHash());
297-
298333
// Update the expected result to know about the new output coins
299-
CCoins &coins = result[tx.GetHash()];
300-
coins.FromTx(tx, height);
334+
result[tx.GetHash()].FromTx(tx, height);
335+
336+
// Call UpdateCoins on the top cache
337+
CTxUndo undo;
338+
UpdateCoins(tx, *(stack.back()), undo, height);
301339

302-
UpdateCoins(tx, *(stack.back()), height);
340+
// Update the utxo set for future spends
341+
utxoset.insert(tx.GetHash());
342+
343+
// Track this tx and undo info to use later
344+
alltxs.insert(std::make_pair(tx.GetHash(),std::make_tuple(tx,undo,oldcoins)));
345+
}
346+
347+
//1/20 times undo a previous transaction
348+
else if (utxoset.size()) {
349+
TxData &txd = FindRandomFrom(utxoset);
350+
351+
CTransaction &tx = std::get<0>(txd);
352+
CTxUndo &undo = std::get<1>(txd);
353+
CCoins &origcoins = std::get<2>(txd);
354+
355+
uint256 undohash = tx.GetHash();
356+
357+
// Update the expected result
358+
// Remove new outputs
359+
result[undohash].Clear();
360+
// If not coinbase restore prevout
361+
if (!tx.IsCoinBase()) {
362+
result[tx.vin[0].prevout.hash] = origcoins;
363+
}
364+
365+
// Disconnect the tx from the current UTXO
366+
// See code in DisconnectBlock
367+
// remove outputs
368+
{
369+
CCoinsModifier outs = stack.back()->ModifyCoins(undohash);
370+
outs->Clear();
371+
}
372+
// restore inputs
373+
if (!tx.IsCoinBase()) {
374+
const COutPoint &out = tx.vin[0].prevout;
375+
const CTxInUndo &undoin = undo.vprevout[0];
376+
ApplyTxInUndo(undoin, *(stack.back()), out);
377+
}
378+
// Store as a candidate for reconnection
379+
disconnectedids.insert(undohash);
380+
381+
// Update the utxoset
382+
utxoset.erase(undohash);
383+
if (!tx.IsCoinBase())
384+
utxoset.insert(tx.vin[0].prevout.hash);
303385
}
304386

305387
// Once every 1000 iterations and at the end, verify the full cache.
@@ -308,9 +390,9 @@ BOOST_AUTO_TEST_CASE(updatecoins_simulation_test)
308390
const CCoins* coins = stack.back()->AccessCoins(it->first);
309391
if (coins) {
310392
BOOST_CHECK(*coins == it->second);
311-
} else {
393+
} else {
312394
BOOST_CHECK(it->second.IsPruned());
313-
}
395+
}
314396
}
315397
}
316398

@@ -334,7 +416,7 @@ BOOST_AUTO_TEST_CASE(updatecoins_simulation_test)
334416
tip = stack.back();
335417
}
336418
stack.push_back(new CCoinsViewCacheTest(tip));
337-
}
419+
}
338420
}
339421
}
340422

@@ -420,6 +502,7 @@ BOOST_AUTO_TEST_CASE(ccoins_serialization)
420502
const static uint256 TXID;
421503
const static CAmount PRUNED = -1;
422504
const static CAmount ABSENT = -2;
505+
const static CAmount FAIL = -3;
423506
const static CAmount VALUE1 = 100;
424507
const static CAmount VALUE2 = 200;
425508
const static CAmount VALUE3 = 300;
@@ -630,11 +713,17 @@ BOOST_AUTO_TEST_CASE(ccoins_modify)
630713
void CheckModifyNewCoinsBase(CAmount base_value, CAmount cache_value, CAmount modify_value, CAmount expected_value, char cache_flags, char expected_flags, bool coinbase)
631714
{
632715
SingleEntryCacheTest test(base_value, cache_value, cache_flags);
633-
SetCoinsValue(modify_value, *test.cache.ModifyNewCoins(TXID, coinbase));
634716

635717
CAmount result_value;
636718
char result_flags;
637-
GetCoinsMapEntry(test.cache.map(), result_value, result_flags);
719+
try {
720+
SetCoinsValue(modify_value, *test.cache.ModifyNewCoins(TXID, coinbase));
721+
GetCoinsMapEntry(test.cache.map(), result_value, result_flags);
722+
} catch (std::logic_error& e) {
723+
result_value = FAIL;
724+
result_flags = NO_ENTRY;
725+
}
726+
638727
BOOST_CHECK_EQUAL(result_value, expected_value);
639728
BOOST_CHECK_EQUAL(result_flags, expected_flags);
640729
}
@@ -669,33 +758,33 @@ BOOST_AUTO_TEST_CASE(ccoins_modify_new)
669758
CheckModifyNewCoins(PRUNED, PRUNED, PRUNED, 0 , DIRTY , true );
670759
CheckModifyNewCoins(PRUNED, PRUNED, ABSENT, FRESH , NO_ENTRY , false);
671760
CheckModifyNewCoins(PRUNED, PRUNED, ABSENT, FRESH , NO_ENTRY , true );
672-
CheckModifyNewCoins(PRUNED, PRUNED, ABSENT, DIRTY , NO_ENTRY , false);
761+
CheckModifyNewCoins(PRUNED, PRUNED, PRUNED, DIRTY , DIRTY , false);
673762
CheckModifyNewCoins(PRUNED, PRUNED, PRUNED, DIRTY , DIRTY , true );
674763
CheckModifyNewCoins(PRUNED, PRUNED, ABSENT, DIRTY|FRESH, NO_ENTRY , false);
675764
CheckModifyNewCoins(PRUNED, PRUNED, ABSENT, DIRTY|FRESH, NO_ENTRY , true );
676765
CheckModifyNewCoins(PRUNED, VALUE3, VALUE3, 0 , DIRTY|FRESH, false);
677766
CheckModifyNewCoins(PRUNED, VALUE3, VALUE3, 0 , DIRTY , true );
678767
CheckModifyNewCoins(PRUNED, VALUE3, VALUE3, FRESH , DIRTY|FRESH, false);
679768
CheckModifyNewCoins(PRUNED, VALUE3, VALUE3, FRESH , DIRTY|FRESH, true );
680-
CheckModifyNewCoins(PRUNED, VALUE3, VALUE3, DIRTY , DIRTY|FRESH, false);
769+
CheckModifyNewCoins(PRUNED, VALUE3, VALUE3, DIRTY , DIRTY , false);
681770
CheckModifyNewCoins(PRUNED, VALUE3, VALUE3, DIRTY , DIRTY , true );
682771
CheckModifyNewCoins(PRUNED, VALUE3, VALUE3, DIRTY|FRESH, DIRTY|FRESH, false);
683772
CheckModifyNewCoins(PRUNED, VALUE3, VALUE3, DIRTY|FRESH, DIRTY|FRESH, true );
684-
CheckModifyNewCoins(VALUE2, PRUNED, ABSENT, 0 , NO_ENTRY , false);
773+
CheckModifyNewCoins(VALUE2, PRUNED, FAIL , 0 , NO_ENTRY , false);
685774
CheckModifyNewCoins(VALUE2, PRUNED, PRUNED, 0 , DIRTY , true );
686-
CheckModifyNewCoins(VALUE2, PRUNED, ABSENT, FRESH , NO_ENTRY , false);
775+
CheckModifyNewCoins(VALUE2, PRUNED, FAIL , FRESH , NO_ENTRY , false);
687776
CheckModifyNewCoins(VALUE2, PRUNED, ABSENT, FRESH , NO_ENTRY , true );
688-
CheckModifyNewCoins(VALUE2, PRUNED, ABSENT, DIRTY , NO_ENTRY , false);
777+
CheckModifyNewCoins(VALUE2, PRUNED, FAIL , DIRTY , NO_ENTRY , false);
689778
CheckModifyNewCoins(VALUE2, PRUNED, PRUNED, DIRTY , DIRTY , true );
690-
CheckModifyNewCoins(VALUE2, PRUNED, ABSENT, DIRTY|FRESH, NO_ENTRY , false);
779+
CheckModifyNewCoins(VALUE2, PRUNED, FAIL , DIRTY|FRESH, NO_ENTRY , false);
691780
CheckModifyNewCoins(VALUE2, PRUNED, ABSENT, DIRTY|FRESH, NO_ENTRY , true );
692-
CheckModifyNewCoins(VALUE2, VALUE3, VALUE3, 0 , DIRTY|FRESH, false);
781+
CheckModifyNewCoins(VALUE2, VALUE3, FAIL , 0 , NO_ENTRY , false);
693782
CheckModifyNewCoins(VALUE2, VALUE3, VALUE3, 0 , DIRTY , true );
694-
CheckModifyNewCoins(VALUE2, VALUE3, VALUE3, FRESH , DIRTY|FRESH, false);
783+
CheckModifyNewCoins(VALUE2, VALUE3, FAIL , FRESH , NO_ENTRY , false);
695784
CheckModifyNewCoins(VALUE2, VALUE3, VALUE3, FRESH , DIRTY|FRESH, true );
696-
CheckModifyNewCoins(VALUE2, VALUE3, VALUE3, DIRTY , DIRTY|FRESH, false);
785+
CheckModifyNewCoins(VALUE2, VALUE3, FAIL , DIRTY , NO_ENTRY , false);
697786
CheckModifyNewCoins(VALUE2, VALUE3, VALUE3, DIRTY , DIRTY , true );
698-
CheckModifyNewCoins(VALUE2, VALUE3, VALUE3, DIRTY|FRESH, DIRTY|FRESH, false);
787+
CheckModifyNewCoins(VALUE2, VALUE3, FAIL , DIRTY|FRESH, NO_ENTRY , false);
699788
CheckModifyNewCoins(VALUE2, VALUE3, VALUE3, DIRTY|FRESH, DIRTY|FRESH, true );
700789
}
701790

src/validation.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1498,7 +1498,7 @@ bool AbortNode(CValidationState& state, const std::string& strMessage, const std
14981498
* @param out The out point that corresponds to the tx input.
14991499
* @return True on success.
15001500
*/
1501-
static bool ApplyTxInUndo(const CTxInUndo& undo, CCoinsViewCache& view, const COutPoint& out)
1501+
bool ApplyTxInUndo(const CTxInUndo& undo, CCoinsViewCache& view, const COutPoint& out)
15021502
{
15031503
bool fClean = true;
15041504

0 commit comments

Comments
 (0)