Skip to content

Commit 137e57b

Browse files
random-zebrafurszy
authored andcommitted
[Validation] Fix AcceptBlock secondary branch validation
1 parent ba18588 commit 137e57b

File tree

2 files changed

+46
-18
lines changed

2 files changed

+46
-18
lines changed

src/validation.cpp

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3120,43 +3120,60 @@ static bool CheckInBlockDoubleSpends(const CBlock& block, int nHeight, CValidati
31203120
* Return false also when the fork is longer than -maxreorg.
31213121
* Return true otherwise.
31223122
* Save in pindexFork the index of the pre-split block (last common block with the active chain).
3123+
* Remove from the outpoints set, any coin that was created in the fork (we don't
3124+
* need to check that it was unspent on the active chain before the split).
31233125
*/
3124-
static bool IsUnspentOnFork(const std::unordered_set<COutPoint, SaltedOutpointHasher>& outpoints,
3126+
static bool IsUnspentOnFork(std::unordered_set<COutPoint, SaltedOutpointHasher>& outpoints,
31253127
const std::set<CBigNum>& serials,
31263128
const CBlockIndex* startIndex, CValidationState& state, const CBlockIndex*& pindexFork)
31273129
{
31283130
// Go backwards on the forked chain up to the split
31293131
int readBlock = 0;
31303132
pindexFork = startIndex;
31313133
for ( ; !chainActive.Contains(pindexFork); pindexFork = pindexFork->pprev) {
3132-
// read block
3133-
CBlock bl;
3134-
if (!ReadBlockFromDisk(bl, pindexFork)) {
3135-
return error("%s: block %s not on disk", __func__, pindexFork->GetBlockHash().GetHex());
3136-
}
31373134
// Check if the forked chain is longer than the max reorg limit
31383135
if (++readBlock == gArgs.GetArg("-maxreorg", DEFAULT_MAX_REORG_DEPTH)) {
31393136
// TODO: Remove this chain from disk.
31403137
return error("%s: forked chain longer than maximum reorg limit", __func__);
31413138
}
3139+
if (pindexFork->pprev == nullptr) {
3140+
return error("%s: null pprev for block %s", __func__, pindexFork->GetBlockHash().GetHex());
3141+
}
3142+
3143+
// if there are no coins left, don't read the block
3144+
if (outpoints.empty() && serials.empty()) continue;
3145+
3146+
// read block
3147+
CBlock bl;
3148+
if (!ReadBlockFromDisk(bl, pindexFork)) {
3149+
return error("%s: block %s not on disk", __func__, pindexFork->GetBlockHash().GetHex());
3150+
}
31423151
// Loop through every tx of this block
3143-
for (const auto& tx : bl.vtx) {
3152+
// (reversed because we first check spent outpoints, and then remove created ones)
3153+
for (auto it = bl.vtx.rbegin(); it != bl.vtx.rend(); ++it) {
3154+
CTransactionRef tx = *it;
31443155
// Loop through every input of this tx
31453156
for (const CTxIn& in: tx->vin) {
31463157
// check if any of the provided outpoints/serials is being spent
31473158
if (!in.IsZerocoinSpend()) {
31483159
// regular utxo
31493160
if (outpoints.find(in.prevout) != outpoints.end()) {
3150-
return state.DoS(100, error("%s: tx inputs spent on forked chain post-split (%s)", __func__, in.prevout.ToString()));
3161+
return state.DoS(100, error("bad-txns-inputs-spent-fork-post-split"));
31513162
}
31523163
} else {
31533164
// zerocoin serial
31543165
const CBigNum& s = TxInToZerocoinSpend(in).getCoinSerialNumber();
31553166
if (serials.find(s) != serials.end()) {
3156-
return state.DoS(100, error("%s: zc serials spent on forked chain post-split", __func__));
3167+
return state.DoS(100, false, REJECT_INVALID, "bad-txns-serials-spent-fork-post-split");
31573168
}
31583169
}
31593170
}
3171+
// Then remove from the outpoints set, any coin created by this tx
3172+
const uint256& txid = tx->GetHash();
3173+
for (size_t i = 0; i < tx->vout.size(); i++) {
3174+
// erase if present (no-op if not)
3175+
outpoints.erase(COutPoint(txid, i));
3176+
}
31603177
}
31613178
}
31623179

@@ -3167,15 +3184,15 @@ static bool IsUnspentOnFork(const std::unordered_set<COutPoint, SaltedOutpointHa
31673184

31683185
/*
31693186
* Check whether ALL the provided inputs (regular utxos) are SPENT on the currently active chain.
3170-
* Start from startIndex and go upwards on the active chain, up to the tip.
3187+
* Start from the block on top of pindexFork, and go upwards on the active chain, up to the tip.
31713188
* Remove from the 'outpoints' set, all the inputs spent by transactions included in the scanned
31723189
* blocks. At the end, return true if the set is empty (all outpoints are spent), and false
31733190
* otherwise (some outpoint is unspent).
31743191
*/
3175-
static bool IsSpentOnActiveChain(std::unordered_set<COutPoint, SaltedOutpointHasher>& outpoints, const CBlockIndex* startIndex)
3192+
static bool IsSpentOnActiveChain(std::unordered_set<COutPoint, SaltedOutpointHasher>& outpoints, const CBlockIndex* pindexFork)
31763193
{
3177-
assert(chainActive.Contains(startIndex));
3178-
const int height_start = startIndex->nHeight;
3194+
assert(chainActive.Contains(pindexFork));
3195+
const int height_start = pindexFork->nHeight + 1;
31793196
const int height_end = chainActive.Height();
31803197

31813198
// Go upwards on the active chain till the tip
@@ -3259,10 +3276,12 @@ static bool AcceptBlock(const CBlock& block, CValidationState& state, CBlockInde
32593276

32603277
// If this is a fork, check if all the tx inputs were spent in the fork
32613278
// Start at the block we're adding on to.
3279+
// Also remove from spent_outpoints any coin that was created in the fork
32623280
const CBlockIndex* pindexFork{nullptr}; // index of the split block (last common block between fork and active chain)
32633281
if (isBlockFromFork && !IsUnspentOnFork(spent_outpoints, spent_serials, pindexPrev, state, pindexFork)) {
32643282
return false;
32653283
}
3284+
assert(!isBlockFromFork || pindexFork != nullptr);
32663285

32673286
// Reject forks below maxdepth
32683287
if (isBlockFromFork && chainActive.Height() - pindexFork->nHeight > gArgs.GetArg("-maxreorg", DEFAULT_MAX_REORG_DEPTH)) {
@@ -3284,6 +3303,10 @@ static bool AcceptBlock(const CBlock& block, CValidationState& state, CBlockInde
32843303
for (auto it = spent_outpoints.begin(); it != spent_outpoints.end(); /* no increment */) {
32853304
const Coin& coin = pcoinsTip->AccessCoin(*it);
32863305
if (!coin.IsSpent()) {
3306+
// if this is on a fork, then the coin must be created before the split
3307+
if (isBlockFromFork && (int) coin.nHeight > pindexFork->nHeight) {
3308+
return state.DoS(100, error("bad-txns-inputs-created-post-split"));
3309+
}
32873310
// unspent on active chain
32883311
it = spent_outpoints.erase(it);
32893312
} else {
@@ -3294,14 +3317,19 @@ static bool AcceptBlock(const CBlock& block, CValidationState& state, CBlockInde
32943317
}
32953318
}
32963319
if (isBlockFromFork && !spent_outpoints.empty()) {
3297-
// Some coins were spent on the active chain.
3320+
// Some coins are not spent on the fork post-split, but cannot be found in the coins cache.
3321+
// So they were either created on the fork, or spent on the active chain.
3322+
// Since coins created in the fork are removed by IsUnspentOnFork(), if there are some coins left,
3323+
// they were spent on the active chain.
3324+
// If some of them was not spent after the split, then the block is invalid.
32983325
// Walk the active chain, starting from pindexFork, going upwards till the chain tip, and check if
32993326
// all of these coins were spent by transactions included in the scanned blocks.
33003327
// If ALL of them are spent, then accept the block.
33013328
// Otherwise reject it, as it means that this blocks includes a transaction with an input that is
33023329
// either already spent before the chain split, or non-existent.
3303-
if (!IsSpentOnActiveChain(spent_outpoints, pindexFork))
3304-
return error("%s: tx inputs spent/not-available on forked chain pre-split", __func__);
3330+
if (!IsSpentOnActiveChain(spent_outpoints, pindexFork)) {
3331+
return state.DoS(100, error("bad-txns-inputs-spent-fork-pre-split"));
3332+
}
33053333
}
33063334

33073335
// ZPOS contextual checks

test/functional/mining_pos_fakestake.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def test_2(self):
149149
prevModifier = self.nodes[1].getblock(prevBlockHash)['stakeModifier']
150150
block = self.stake_block(1, 7, 258, prevBlockHash, prevModifier, "0",
151151
self.get_prevouts(1, list(self.utxos_to_spend)), self.mocktime, "", [], False)
152-
self.send_block_and_check_error(block, "tx inputs spent/not-available on forked chain pre-split")
152+
self.send_block_and_check_error(block, "bad-txns-inputs-spent-fork-pre-split")
153153
assert_equal(self.nodes[1].getblockcount(), 260)
154154
self.log.info("--> Test_2 passed")
155155

@@ -245,7 +245,7 @@ def get_prev_modifier(prevBlockHash):
245245
elif isMainChain:
246246
reject_log = "tx inputs spent/not-available on main chain"
247247
else:
248-
reject_log = "tx inputs spent on forked chain post-split"
248+
reject_log = "bad-txns-inputs-spent-fork-post-split"
249249
self.send_block_and_check_error(block, reject_log)
250250
else:
251251
var = self.nodes[1].submitblock(bytes_to_hex_str(block.serialize()))

0 commit comments

Comments
 (0)