Merge bitcoin/bitcoin#29370: assumeutxo: Get rid of faked nTx and nChainTx values

9d9a7458a2 assumeutxo: Remove BLOCK_ASSUMED_VALID flag (Ryan Ofsky)
ef174e9ed2 test: assumeutxo snapshot block CheckBlockIndex crash test (Ryan Ofsky)
0391458d76 test: assumeutxo stale block CheckBlockIndex crash test (Ryan Ofsky)
ef29c8b662 assumeutxo: Get rid of faked nTx and nChainTx values (Ryan Ofsky)
9b97d5bbf9 doc: Improve comments describing setBlockIndexCandidates checks (Ryan Ofsky)
0fd915ee6b validation: Check GuessVerificationProgress is not called with disconnected block (Ryan Ofsky)
63e8fc912c ci: add getchaintxstats ubsan suppressions (Ryan Ofsky)
f252e687ec assumeutxo test: Add RPC test for fake nTx and nChainTx values (Ryan Ofsky)

Pull request description:

  The `PopulateAndValidateSnapshot` function introduced in f6e2da5fb7 from #19806 has been setting fake `nTx` and `nChainTx` values that can show up in RPC results (https://github.com/bitcoin/bitcoin/issues/29328) and make `CBlockIndex` state hard to reason about, because it is difficult to know whether the values are real or fake.

  Revert to previous behavior of setting `nTx` and `nChainTx` to 0 when the values are unknown, instead of faking them. Also drop no-longer needed `BLOCK_ASSUMED_VALID` flag.

  Dropping the faked values also fixes assert failures in the `CheckBlockIndex` `(pindex->nChainTx == pindex->nTx + prev_chain_tx)` check that could happen previously if forked or out-of-order blocks before the snapshot got submitted while the snapshot was being validated. The PR includes two commits adding tests for these failures and describing them in detail.

  Compatibility note: This change could cause new `-checkblockindex` failures if a snapshot was loaded by a previous version of Bitcoin Core and not fully validated, because fake `nTx` values will have been saved to the block index. It would be pretty easy to avoid these failures by adding some compatibility code to `LoadBlockIndex` and changing `nTx` values from 1 to 0 when they are fake (when `(pindex->nStatus & BLOCK_VALID_MASK) < BLOCK_VALID_TRANSACTIONS`), but a little simpler not to worry about being compatible in this case.

ACKs for top commit:
  Sjors:
    re-ACK 9d9a7458a2
  achow101:
    ACK 9d9a7458a2
  mzumsande:
    Tested ACK 9d9a7458a2
  maflcko:
    ACK 9d9a7458a2 🎯

Tree-SHA512: b1e1e2731ec36be30d5f5914042517219378fc31486674030c29d9c7488ed83fb60ba7095600f469dc32f0d8ba79c49ff7706303006507654e1762f26ee416e0
This commit is contained in:
Ava Chow
2024-03-20 12:49:19 -04:00
8 changed files with 231 additions and 156 deletions

View File

@@ -34,6 +34,7 @@ Interesting starting states could be loading a snapshot when the current chain t
"""
from shutil import rmtree
from dataclasses import dataclass
from test_framework.messages import tx_from_hex
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
@@ -174,10 +175,26 @@ class AssumeutxoTest(BitcoinTestFramework):
# Generate a series of blocks that `n0` will have in the snapshot,
# but that n1 and n2 don't yet see.
assert n0.getblockcount() == START_HEIGHT
blocks = {START_HEIGHT: Block(n0.getbestblockhash(), 1, START_HEIGHT + 1)}
for i in range(100):
block_tx = 1
if i % 3 == 0:
self.mini_wallet.send_self_transfer(from_node=n0)
block_tx += 1
self.generate(n0, nblocks=1, sync_fun=self.no_op)
height = n0.getblockcount()
hash = n0.getbestblockhash()
blocks[height] = Block(hash, block_tx, blocks[height-1].chain_tx + block_tx)
if i == 4:
# Create a stale block that forks off the main chain before the snapshot.
temp_invalid = n0.getbestblockhash()
n0.invalidateblock(temp_invalid)
stale_hash = self.generateblock(n0, output="raw(aaaa)", transactions=[], sync_fun=self.no_op)["hash"]
n0.invalidateblock(stale_hash)
n0.reconsiderblock(temp_invalid)
stale_block = n0.getblock(stale_hash, 0)
self.log.info("-- Testing assumeutxo + some indexes + pruning")
@@ -207,7 +224,7 @@ class AssumeutxoTest(BitcoinTestFramework):
assert_equal(
dump_output['txoutset_hash'],
"a4bf3407ccb2cc0145c49ebba8fa91199f8a3903daf0883875941497d2493c27")
assert_equal(dump_output["nchaintx"], 334)
assert_equal(dump_output["nchaintx"], blocks[SNAPSHOT_BASE_HEIGHT].chain_tx)
assert_equal(n0.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT)
# Mine more blocks on top of the snapshot that n1 hasn't yet seen. This
@@ -228,6 +245,29 @@ class AssumeutxoTest(BitcoinTestFramework):
assert_equal(loaded['coins_loaded'], SNAPSHOT_BASE_HEIGHT)
assert_equal(loaded['base_height'], SNAPSHOT_BASE_HEIGHT)
def check_tx_counts(final: bool) -> None:
"""Check nTx and nChainTx intermediate values right after loading
the snapshot, and final values after the snapshot is validated."""
for height, block in blocks.items():
tx = n1.getblockheader(block.hash)["nTx"]
chain_tx = n1.getchaintxstats(nblocks=1, blockhash=block.hash)["txcount"]
# Intermediate nTx of the starting block should be set, but nTx of
# later blocks should be 0 before they are downloaded.
if final or height == START_HEIGHT:
assert_equal(tx, block.tx)
else:
assert_equal(tx, 0)
# Intermediate nChainTx of the starting block and snapshot block
# should be set, but others should be 0 until they are downloaded.
if final or height in (START_HEIGHT, SNAPSHOT_BASE_HEIGHT):
assert_equal(chain_tx, block.chain_tx)
else:
assert_equal(chain_tx, 0)
check_tx_counts(final=False)
normal, snapshot = n1.getchainstates()["chainstates"]
assert_equal(normal['blocks'], START_HEIGHT)
assert_equal(normal.get('snapshot_blockhash'), None)
@@ -238,6 +278,15 @@ class AssumeutxoTest(BitcoinTestFramework):
assert_equal(n1.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT)
self.log.info("Submit a stale block that forked off the chain before the snapshot")
# Normally a block like this would not be downloaded, but if it is
# submitted early before the background chain catches up to the fork
# point, it winds up in m_blocks_unlinked and triggers a corner case
# that previously crashed CheckBlockIndex.
n1.submitblock(stale_block)
n1.getchaintips()
n1.getblock(stale_hash)
self.log.info("Submit a spending transaction for a snapshot chainstate coin to the mempool")
# spend the coinbase output of the first block that is not available on node1
spend_coin_blockhash = n1.getblockhash(START_HEIGHT + 1)
@@ -275,6 +324,16 @@ class AssumeutxoTest(BitcoinTestFramework):
self.log.info("Restarted node before snapshot validation completed, reloading...")
self.restart_node(1, extra_args=self.extra_args[1])
# Send snapshot block to n1 out of order. This makes the test less
# realistic because normally the snapshot block is one of the last
# blocks downloaded, but its useful to test because it triggers more
# corner cases in ReceivedBlockTransactions() and CheckBlockIndex()
# setting and testing nChainTx values, and it exposed previous bugs.
snapshot_hash = n0.getblockhash(SNAPSHOT_BASE_HEIGHT)
snapshot_block = n0.getblock(snapshot_hash, 0)
n1.submitblock(snapshot_block)
self.connect_nodes(0, 1)
self.log.info(f"Ensuring snapshot chain syncs to tip. ({FINAL_HEIGHT})")
@@ -291,6 +350,8 @@ class AssumeutxoTest(BitcoinTestFramework):
}
self.wait_until(lambda: n1.getindexinfo() == completed_idx_state)
self.log.info("Re-check nTx and nChainTx values")
check_tx_counts(final=True)
for i in (0, 1):
n = self.nodes[i]
@@ -365,6 +426,11 @@ class AssumeutxoTest(BitcoinTestFramework):
self.connect_nodes(0, 2)
self.wait_until(lambda: n2.getblockcount() == FINAL_HEIGHT)
@dataclass
class Block:
hash: str
tx: int
chain_tx: int
if __name__ == '__main__':
AssumeutxoTest().main()

View File

@@ -51,6 +51,7 @@ unsigned-integer-overflow:CCoinsViewCache::Uncache
unsigned-integer-overflow:CompressAmount
unsigned-integer-overflow:DecompressAmount
unsigned-integer-overflow:crypto/
unsigned-integer-overflow:getchaintxstats*
unsigned-integer-overflow:MurmurHash3
unsigned-integer-overflow:CBlockPolicyEstimator::processBlockTx
unsigned-integer-overflow:TxConfirmStats::EstimateMedianVal
@@ -61,6 +62,7 @@ implicit-integer-sign-change:CBlockPolicyEstimator::processBlockTx
implicit-integer-sign-change:SetStdinEcho
implicit-integer-sign-change:compressor.h
implicit-integer-sign-change:crypto/
implicit-integer-sign-change:getchaintxstats*
implicit-integer-sign-change:TxConfirmStats::removeTx
implicit-integer-sign-change:prevector.h
implicit-integer-sign-change:verify_flags