Browse Source

Merge #11531: Check that new headers are not a descendant of an invalid block (more effeciently)

f3d4adf Make p2p-acceptablock not an extended test (Matt Corallo)
00dcda6 [qa] test that invalid blocks on an invalid chain get a disconnect (Matt Corallo)
015a525 Reject headers building on invalid chains by tracking invalidity (Matt Corallo)
932f118 Accept unrequested blocks with work equal to our tip (Matt Corallo)
3d9c70c Stop always storing blocks from whitelisted peers (Matt Corallo)
3b4ac43 Rewrite p2p-acceptblock in preparation for slight behavior changes (Matt Corallo)

Pull request description:

  @sdaftuar pointed out that the version in #11487 was somewhat DoS-able as someone could feed you a valid chain that forked off the the last checkpoint block and force you to do lots of work just walking backwards across blocks for each new block they gave you. We came up with a few proposals but settled on the one implemented here as likely the simplest without obvious DoS issues. It uses our existing on-load mapBlockIndex walk to make sure everything that descends from an invalid block is marked as such, and then simply caches blocks which we attempted to connect but which were found to be invalid. To avoid DoS issues during IBD, this will need to depend on #11458.

  Includes tests from #11487.

Tree-SHA512: 46aff8332908e122dae72ceb5fe8cd241902c2281a87f58a5fb486bf69d46458d84a096fdcb5f3e8e07fbcf7466232b10c429f4d67855425f11b38ac0bf612e1
0.16
Wladimir J. van der Laan 7 years ago
parent
commit
cffa5ee132
No known key found for this signature in database
GPG Key ID: 1E4AED62986CD25D
  1. 6
      src/net_processing.cpp
  2. 78
      src/validation.cpp
  3. 309
      test/functional/p2p-acceptblock.py
  4. 2
      test/functional/test_runner.py

6
src/net_processing.cpp

@ -2531,11 +2531,7 @@ bool static ProcessMessage(CNode* pfrom, const std::string& strCommand, CDataStr
LogPrint(BCLog::NET, "received block %s peer=%d\n", pblock->GetHash().ToString(), pfrom->GetId()); LogPrint(BCLog::NET, "received block %s peer=%d\n", pblock->GetHash().ToString(), pfrom->GetId());
// Process all blocks from whitelisted peers, even if not requested, bool forceProcessing = false;
// unless we're still syncing with the network.
// Such an unrequested block may still be processed, subject to the
// conditions in AcceptBlock().
bool forceProcessing = pfrom->fWhitelisted && !IsInitialBlockDownload();
const uint256 hash(pblock->GetHash()); const uint256 hash(pblock->GetHash());
{ {
LOCK(cs_main); LOCK(cs_main);

78
src/validation.cpp

@ -156,6 +156,26 @@ namespace {
/** chainwork for the last block that preciousblock has been applied to. */ /** chainwork for the last block that preciousblock has been applied to. */
arith_uint256 nLastPreciousChainwork = 0; arith_uint256 nLastPreciousChainwork = 0;
/** In order to efficiently track invalidity of headers, we keep the set of
* blocks which we tried to connect and found to be invalid here (ie which
* were set to BLOCK_FAILED_VALID since the last restart). We can then
* walk this set and check if a new header is a descendant of something in
* this set, preventing us from having to walk mapBlockIndex when we try
* to connect a bad block and fail.
*
* While this is more complicated than marking everything which descends
* from an invalid block as invalid at the time we discover it to be
* invalid, doing so would require walking all of mapBlockIndex to find all
* descendants. Since this case should be very rare, keeping track of all
* BLOCK_FAILED_VALID blocks in a set should be just fine and work just as
* well.
*
* Because we alreardy walk mapBlockIndex in height-order at startup, we go
* ahead and mark descendants of invalid blocks as FAILED_CHILD at that time,
* instead of putting things in this set.
*/
std::set<CBlockIndex*> g_failed_blocks;
/** Dirty block index entries. */ /** Dirty block index entries. */
std::set<CBlockIndex*> setDirtyBlockIndex; std::set<CBlockIndex*> setDirtyBlockIndex;
@ -1180,6 +1200,7 @@ void static InvalidChainFound(CBlockIndex* pindexNew)
void static InvalidBlockFound(CBlockIndex *pindex, const CValidationState &state) { void static InvalidBlockFound(CBlockIndex *pindex, const CValidationState &state) {
if (!state.CorruptionPossible()) { if (!state.CorruptionPossible()) {
pindex->nStatus |= BLOCK_FAILED_VALID; pindex->nStatus |= BLOCK_FAILED_VALID;
g_failed_blocks.insert(pindex);
setDirtyBlockIndex.insert(pindex); setDirtyBlockIndex.insert(pindex);
setBlockIndexCandidates.erase(pindex); setBlockIndexCandidates.erase(pindex);
InvalidChainFound(pindex); InvalidChainFound(pindex);
@ -2533,17 +2554,18 @@ bool InvalidateBlock(CValidationState& state, const CChainParams& chainparams, C
{ {
AssertLockHeld(cs_main); AssertLockHeld(cs_main);
// Mark the block itself as invalid. // We first disconnect backwards and then mark the blocks as invalid.
pindex->nStatus |= BLOCK_FAILED_VALID; // This prevents a case where pruned nodes may fail to invalidateblock
setDirtyBlockIndex.insert(pindex); // and be left unable to start as they have no tip candidates (as there
setBlockIndexCandidates.erase(pindex); // are no blocks that meet the "have data and are not invalid per
// nStatus" criteria for inclusion in setBlockIndexCandidates).
bool pindex_was_in_chain = false;
CBlockIndex *invalid_walk_tip = chainActive.Tip();
DisconnectedBlockTransactions disconnectpool; DisconnectedBlockTransactions disconnectpool;
while (chainActive.Contains(pindex)) { while (chainActive.Contains(pindex)) {
CBlockIndex *pindexWalk = chainActive.Tip(); pindex_was_in_chain = true;
pindexWalk->nStatus |= BLOCK_FAILED_CHILD;
setDirtyBlockIndex.insert(pindexWalk);
setBlockIndexCandidates.erase(pindexWalk);
// ActivateBestChain considers blocks already in chainActive // ActivateBestChain considers blocks already in chainActive
// unconditionally valid already, so force disconnect away from it. // unconditionally valid already, so force disconnect away from it.
if (!DisconnectTip(state, chainparams, &disconnectpool)) { if (!DisconnectTip(state, chainparams, &disconnectpool)) {
@ -2554,6 +2576,21 @@ bool InvalidateBlock(CValidationState& state, const CChainParams& chainparams, C
} }
} }
// Now mark the blocks we just disconnected as descendants invalid
// (note this may not be all descendants).
while (pindex_was_in_chain && invalid_walk_tip != pindex) {
invalid_walk_tip->nStatus |= BLOCK_FAILED_CHILD;
setDirtyBlockIndex.insert(invalid_walk_tip);
setBlockIndexCandidates.erase(invalid_walk_tip);
invalid_walk_tip = invalid_walk_tip->pprev;
}
// Mark the block itself as invalid.
pindex->nStatus |= BLOCK_FAILED_VALID;
setDirtyBlockIndex.insert(pindex);
setBlockIndexCandidates.erase(pindex);
g_failed_blocks.insert(pindex);
// DisconnectTip will add transactions to disconnectpool; try to add these // DisconnectTip will add transactions to disconnectpool; try to add these
// back to the mempool. // back to the mempool.
UpdateMempoolForReorg(disconnectpool, true); UpdateMempoolForReorg(disconnectpool, true);
@ -2591,6 +2628,7 @@ bool ResetBlockFailureFlags(CBlockIndex *pindex) {
// Reset invalid block marker if it was pointing to one of those. // Reset invalid block marker if it was pointing to one of those.
pindexBestInvalid = nullptr; pindexBestInvalid = nullptr;
} }
g_failed_blocks.erase(it->second);
} }
it++; it++;
} }
@ -3066,6 +3104,21 @@ static bool AcceptBlockHeader(const CBlockHeader& block, CValidationState& state
return state.DoS(100, error("%s: prev block invalid", __func__), REJECT_INVALID, "bad-prevblk"); return state.DoS(100, error("%s: prev block invalid", __func__), REJECT_INVALID, "bad-prevblk");
if (!ContextualCheckBlockHeader(block, state, chainparams, pindexPrev, GetAdjustedTime())) if (!ContextualCheckBlockHeader(block, state, chainparams, pindexPrev, GetAdjustedTime()))
return error("%s: Consensus::ContextualCheckBlockHeader: %s, %s", __func__, hash.ToString(), FormatStateMessage(state)); return error("%s: Consensus::ContextualCheckBlockHeader: %s, %s", __func__, hash.ToString(), FormatStateMessage(state));
if (!pindexPrev->IsValid(BLOCK_VALID_SCRIPTS)) {
for (const CBlockIndex* failedit : g_failed_blocks) {
if (pindexPrev->GetAncestor(failedit->nHeight) == failedit) {
assert(failedit->nStatus & BLOCK_FAILED_VALID);
CBlockIndex* invalid_walk = pindexPrev;
while (invalid_walk != failedit) {
invalid_walk->nStatus |= BLOCK_FAILED_CHILD;
setDirtyBlockIndex.insert(invalid_walk);
invalid_walk = invalid_walk->pprev;
}
return state.DoS(100, error("%s: prev block invalid", __func__), REJECT_INVALID, "bad-prevblk");
}
}
}
} }
if (pindex == nullptr) if (pindex == nullptr)
pindex = AddToBlockIndex(block); pindex = AddToBlockIndex(block);
@ -3117,7 +3170,7 @@ static bool AcceptBlock(const std::shared_ptr<const CBlock>& pblock, CValidation
// process an unrequested block if it's new and has enough work to // process an unrequested block if it's new and has enough work to
// advance our tip, and isn't too many blocks ahead. // advance our tip, and isn't too many blocks ahead.
bool fAlreadyHave = pindex->nStatus & BLOCK_HAVE_DATA; bool fAlreadyHave = pindex->nStatus & BLOCK_HAVE_DATA;
bool fHasMoreWork = (chainActive.Tip() ? pindex->nChainWork > chainActive.Tip()->nChainWork : true); bool fHasMoreOrSameWork = (chainActive.Tip() ? pindex->nChainWork >= chainActive.Tip()->nChainWork : true);
// Blocks that are too out-of-order needlessly limit the effectiveness of // Blocks that are too out-of-order needlessly limit the effectiveness of
// pruning, because pruning will not delete block files that contain any // pruning, because pruning will not delete block files that contain any
// blocks which are too close in height to the tip. Apply this test // blocks which are too close in height to the tip. Apply this test
@ -3135,7 +3188,7 @@ static bool AcceptBlock(const std::shared_ptr<const CBlock>& pblock, CValidation
if (fAlreadyHave) return true; if (fAlreadyHave) return true;
if (!fRequested) { // If we didn't ask for it: if (!fRequested) { // If we didn't ask for it:
if (pindex->nTx != 0) return true; // This is a previously-processed block that was pruned if (pindex->nTx != 0) return true; // This is a previously-processed block that was pruned
if (!fHasMoreWork) return true; // Don't process less-work chains if (!fHasMoreOrSameWork) return true; // Don't process less-work chains
if (fTooFarAhead) return true; // Block height is too high if (fTooFarAhead) return true; // Block height is too high
// Protect against DoS attacks from low-work chains. // Protect against DoS attacks from low-work chains.
@ -3494,6 +3547,10 @@ bool static LoadBlockIndexDB(const CChainParams& chainparams)
pindex->nChainTx = pindex->nTx; pindex->nChainTx = pindex->nTx;
} }
} }
if (!(pindex->nStatus & BLOCK_FAILED_MASK) && pindex->pprev && (pindex->pprev->nStatus & BLOCK_FAILED_MASK)) {
pindex->nStatus |= BLOCK_FAILED_CHILD;
setDirtyBlockIndex.insert(pindex);
}
if (pindex->IsValid(BLOCK_VALID_TRANSACTIONS) && (pindex->nChainTx || pindex->pprev == nullptr)) if (pindex->IsValid(BLOCK_VALID_TRANSACTIONS) && (pindex->nChainTx || pindex->pprev == nullptr))
setBlockIndexCandidates.insert(pindex); setBlockIndexCandidates.insert(pindex);
if (pindex->nStatus & BLOCK_FAILED_MASK && (!pindexBestInvalid || pindex->nChainWork > pindexBestInvalid->nChainWork)) if (pindex->nStatus & BLOCK_FAILED_MASK && (!pindexBestInvalid || pindex->nChainWork > pindexBestInvalid->nChainWork))
@ -3884,6 +3941,7 @@ void UnloadBlockIndex()
nLastBlockFile = 0; nLastBlockFile = 0;
nBlockSequenceId = 1; nBlockSequenceId = 1;
setDirtyBlockIndex.clear(); setDirtyBlockIndex.clear();
g_failed_blocks.clear();
setDirtyFileInfo.clear(); setDirtyFileInfo.clear();
versionbitscache.Clear(); versionbitscache.Clear();
for (int b = 0; b < VERSIONBITS_NUM_BITS; b++) { for (int b = 0; b < VERSIONBITS_NUM_BITS; b++) {

309
test/functional/p2p-acceptblock.py

@ -4,42 +4,32 @@
# file COPYING or http://www.opensource.org/licenses/mit-license.php. # file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test processing of unrequested blocks. """Test processing of unrequested blocks.
Since behavior differs when receiving unrequested blocks from whitelisted peers Setup: two nodes, node0+node1, not connected to each other. Node1 will have
versus non-whitelisted peers, this tests the behavior of both (effectively two nMinimumChainWork set to 0x10, so it won't process low-work unrequested blocks.
separate tests running in parallel).
Setup: three nodes, node0+node1+node2, not connected to each other. Node0 does not We have one NodeConn connection to node0 called test_node, and one to node1
whitelist localhost, but node1 does. They will each be on their own chain for called min_work_node.
this test. Node2 will have nMinimumChainWork set to 0x10, so it won't process
low-work unrequested blocks.
We have one NodeConn connection to each, test_node, white_node, and min_work_node,
respectively.
The test: The test:
1. Generate one block on each node, to leave IBD. 1. Generate one block on each node, to leave IBD.
2. Mine a new block on each tip, and deliver to each node from node's peer. 2. Mine a new block on each tip, and deliver to each node from node's peer.
The tip should advance for node0 and node1, but node2 should skip processing The tip should advance for node0, but node1 should skip processing due to
due to nMinimumChainWork. nMinimumChainWork.
Node2 is unused in tests 3-7: Node1 is unused in tests 3-7:
3. Mine a block that forks the previous block, and deliver to each node from 3. Mine a block that forks from the genesis block, and deliver to test_node.
corresponding peer. Node0 should not process this block (just accept the header), because it
Node0 should not process this block (just accept the header), because it is is unrequested and doesn't have more or equal work to the tip.
unrequested and doesn't have more work than the tip.
Node1 should process because this is coming from a whitelisted peer.
4. Send another block that builds on the forking block. 4a,b. Send another two blocks that build on the forking block.
Node0 should process this block but be stuck on the shorter chain, because Node0 should process the second block but be stuck on the shorter chain,
it's missing an intermediate block. because it's missing an intermediate block.
Node1 should reorg to this longer chain.
4b.Send 288 more blocks on the longer chain. 4c.Send 288 more blocks on the longer chain (the number of blocks ahead
we currently store).
Node0 should process all but the last block (too far ahead in height). Node0 should process all but the last block (too far ahead in height).
Send all headers to Node1, and then send the last block in that chain.
Node1 should accept the block because it's coming from a whitelisted peer.
5. Send a duplicate of the block in #3 to Node0. 5. Send a duplicate of the block in #3 to Node0.
Node0 should not process the block because it is unrequested, and stay on Node0 should not process the block because it is unrequested, and stay on
@ -52,16 +42,20 @@ Node2 is unused in tests 3-7:
7. Send Node0 the missing block again. 7. Send Node0 the missing block again.
Node0 should process and the tip should advance. Node0 should process and the tip should advance.
8. Test Node2 is able to sync when connected to node0 (which should have sufficient 8. Create a fork which is invalid at a height longer than the current chain
work on its chain). (ie to which the node will try to reorg) but which has headers built on top
of the invalid block. Check that we get disconnected if we send more headers
on the chain the node now knows to be invalid.
9. Test Node1 is able to sync when connected to node0 (which should have sufficient
work on its chain).
""" """
from test_framework.mininode import * from test_framework.mininode import *
from test_framework.test_framework import BitcoinTestFramework from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import * from test_framework.util import *
import time import time
from test_framework.blocktools import create_block, create_coinbase from test_framework.blocktools import create_block, create_coinbase, create_transaction
class AcceptBlockTest(BitcoinTestFramework): class AcceptBlockTest(BitcoinTestFramework):
def add_options(self, parser): def add_options(self, parser):
@ -71,8 +65,8 @@ class AcceptBlockTest(BitcoinTestFramework):
def set_test_params(self): def set_test_params(self):
self.setup_clean_chain = True self.setup_clean_chain = True
self.num_nodes = 3 self.num_nodes = 2
self.extra_args = [[], ["-whitelist=127.0.0.1"], ["-minimumchainwork=0x10"]] self.extra_args = [[], ["-minimumchainwork=0x10"]]
def setup_network(self): def setup_network(self):
# Node0 will be used to test behavior of processing unrequested blocks # Node0 will be used to test behavior of processing unrequested blocks
@ -84,132 +78,147 @@ class AcceptBlockTest(BitcoinTestFramework):
def run_test(self): def run_test(self):
# Setup the p2p connections and start up the network thread. # Setup the p2p connections and start up the network thread.
test_node = NodeConnCB() # connects to node0 (not whitelisted) test_node = NodeConnCB() # connects to node0
white_node = NodeConnCB() # connects to node1 (whitelisted) min_work_node = NodeConnCB() # connects to node1
min_work_node = NodeConnCB() # connects to node2 (not whitelisted)
connections = [] connections = []
connections.append(NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], test_node)) connections.append(NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], test_node))
connections.append(NodeConn('127.0.0.1', p2p_port(1), self.nodes[1], white_node)) connections.append(NodeConn('127.0.0.1', p2p_port(1), self.nodes[1], min_work_node))
connections.append(NodeConn('127.0.0.1', p2p_port(2), self.nodes[2], min_work_node))
test_node.add_connection(connections[0]) test_node.add_connection(connections[0])
white_node.add_connection(connections[1]) min_work_node.add_connection(connections[1])
min_work_node.add_connection(connections[2])
NetworkThread().start() # Start up network handling in another thread NetworkThread().start() # Start up network handling in another thread
# Test logic begins here # Test logic begins here
test_node.wait_for_verack() test_node.wait_for_verack()
white_node.wait_for_verack()
min_work_node.wait_for_verack() min_work_node.wait_for_verack()
# 1. Have nodes mine a block (nodes1/2 leave IBD) # 1. Have nodes mine a block (leave IBD)
[ n.generate(1) for n in self.nodes ] [ n.generate(1) for n in self.nodes ]
tips = [ int("0x" + n.getbestblockhash(), 0) for n in self.nodes ] tips = [ int("0x" + n.getbestblockhash(), 0) for n in self.nodes ]
# 2. Send one block that builds on each tip. # 2. Send one block that builds on each tip.
# This should be accepted by nodes 1/2 # This should be accepted by node0
blocks_h2 = [] # the height 2 blocks on each node's chain blocks_h2 = [] # the height 2 blocks on each node's chain
block_time = int(time.time()) + 1 block_time = int(time.time()) + 1
for i in range(3): for i in range(2):
blocks_h2.append(create_block(tips[i], create_coinbase(2), block_time)) blocks_h2.append(create_block(tips[i], create_coinbase(2), block_time))
blocks_h2[i].solve() blocks_h2[i].solve()
block_time += 1 block_time += 1
test_node.send_message(msg_block(blocks_h2[0])) test_node.send_message(msg_block(blocks_h2[0]))
white_node.send_message(msg_block(blocks_h2[1])) min_work_node.send_message(msg_block(blocks_h2[1]))
min_work_node.send_message(msg_block(blocks_h2[2]))
for x in [test_node, white_node, min_work_node]: for x in [test_node, min_work_node]:
x.sync_with_ping() x.sync_with_ping()
assert_equal(self.nodes[0].getblockcount(), 2) assert_equal(self.nodes[0].getblockcount(), 2)
assert_equal(self.nodes[1].getblockcount(), 2) assert_equal(self.nodes[1].getblockcount(), 1)
assert_equal(self.nodes[2].getblockcount(), 1) self.log.info("First height 2 block accepted by node0; correctly rejected by node1")
self.log.info("First height 2 block accepted by node0/node1; correctly rejected by node2")
# 3. Send another block that builds on the original tip. # 3. Send another block that builds on genesis.
blocks_h2f = [] # Blocks at height 2 that fork off the main chain block_h1f = create_block(int("0x" + self.nodes[0].getblockhash(0), 0), create_coinbase(1), block_time)
for i in range(2): block_time += 1
blocks_h2f.append(create_block(tips[i], create_coinbase(2), blocks_h2[i].nTime+1)) block_h1f.solve()
blocks_h2f[i].solve() test_node.send_message(msg_block(block_h1f))
test_node.send_message(msg_block(blocks_h2f[0]))
white_node.send_message(msg_block(blocks_h2f[1]))
for x in [test_node, white_node]: test_node.sync_with_ping()
x.sync_with_ping() tip_entry_found = False
for x in self.nodes[0].getchaintips(): for x in self.nodes[0].getchaintips():
if x['hash'] == blocks_h2f[0].hash: if x['hash'] == block_h1f.hash:
assert_equal(x['status'], "headers-only") assert_equal(x['status'], "headers-only")
tip_entry_found = True
assert(tip_entry_found)
assert_raises_rpc_error(-1, "Block not found on disk", self.nodes[0].getblock, block_h1f.hash)
for x in self.nodes[1].getchaintips(): # 4. Send another two block that build on the fork.
if x['hash'] == blocks_h2f[1].hash: block_h2f = create_block(block_h1f.sha256, create_coinbase(2), block_time)
assert_equal(x['status'], "valid-headers") block_time += 1
block_h2f.solve()
self.log.info("Second height 2 block accepted only from whitelisted peer") test_node.send_message(msg_block(block_h2f))
# 4. Now send another block that builds on the forking chain.
blocks_h3 = []
for i in range(2):
blocks_h3.append(create_block(blocks_h2f[i].sha256, create_coinbase(3), blocks_h2f[i].nTime+1))
blocks_h3[i].solve()
test_node.send_message(msg_block(blocks_h3[0]))
white_node.send_message(msg_block(blocks_h3[1]))
for x in [test_node, white_node]: test_node.sync_with_ping()
x.sync_with_ping() # Since the earlier block was not processed by node, the new block
# Since the earlier block was not processed by node0, the new block
# can't be fully validated. # can't be fully validated.
tip_entry_found = False
for x in self.nodes[0].getchaintips(): for x in self.nodes[0].getchaintips():
if x['hash'] == blocks_h3[0].hash: if x['hash'] == block_h2f.hash:
assert_equal(x['status'], "headers-only") assert_equal(x['status'], "headers-only")
tip_entry_found = True
assert(tip_entry_found)
# But this block should be accepted by node0 since it has more work. # But this block should be accepted by node since it has equal work.
self.nodes[0].getblock(blocks_h3[0].hash) self.nodes[0].getblock(block_h2f.hash)
self.log.info("Unrequested more-work block accepted from non-whitelisted peer") self.log.info("Second height 2 block accepted, but not reorg'ed to")
# Node1 should have accepted and reorged. # 4b. Now send another block that builds on the forking chain.
assert_equal(self.nodes[1].getblockcount(), 3) block_h3 = create_block(block_h2f.sha256, create_coinbase(3), block_h2f.nTime+1)
self.log.info("Successfully reorged to length 3 chain from whitelisted peer") block_h3.solve()
test_node.send_message(msg_block(block_h3))
# 4b. Now mine 288 more blocks and deliver; all should be processed but test_node.sync_with_ping()
# the last (height-too-high) on node0. Node1 should process the tip if # Since the earlier block was not processed by node, the new block
# we give it the headers chain leading to the tip. # can't be fully validated.
tips = blocks_h3 tip_entry_found = False
headers_message = msg_headers() for x in self.nodes[0].getchaintips():
all_blocks = [] # node0's blocks if x['hash'] == block_h3.hash:
for j in range(2): assert_equal(x['status'], "headers-only")
tip_entry_found = True
assert(tip_entry_found)
self.nodes[0].getblock(block_h3.hash)
# But this block should be accepted by node since it has more work.
self.nodes[0].getblock(block_h3.hash)
self.log.info("Unrequested more-work block accepted")
# 4c. Now mine 288 more blocks and deliver; all should be processed but
# the last (height-too-high) on node (as long as its not missing any headers)
tip = block_h3
all_blocks = []
for i in range(288): for i in range(288):
next_block = create_block(tips[j].sha256, create_coinbase(i + 4), tips[j].nTime+1) next_block = create_block(tip.sha256, create_coinbase(i + 4), tip.nTime+1)
next_block.solve() next_block.solve()
if j==0:
test_node.send_message(msg_block(next_block))
all_blocks.append(next_block) all_blocks.append(next_block)
else: tip = next_block
headers_message.headers.append(CBlockHeader(next_block))
tips[j] = next_block # Now send the block at height 5 and check that it wasn't accepted (missing header)
test_node.send_message(msg_block(all_blocks[1]))
test_node.sync_with_ping()
assert_raises_rpc_error(-5, "Block not found", self.nodes[0].getblock, all_blocks[1].hash)
assert_raises_rpc_error(-5, "Block not found", self.nodes[0].getblockheader, all_blocks[1].hash)
# The block at height 5 should be accepted if we provide the missing header, though
headers_message = msg_headers()
headers_message.headers.append(CBlockHeader(all_blocks[0]))
test_node.send_message(headers_message)
test_node.send_message(msg_block(all_blocks[1]))
test_node.sync_with_ping()
self.nodes[0].getblock(all_blocks[1].hash)
# Now send the blocks in all_blocks
for i in range(288):
test_node.send_message(msg_block(all_blocks[i]))
test_node.sync_with_ping()
time.sleep(2)
# Blocks 1-287 should be accepted, block 288 should be ignored because it's too far ahead # Blocks 1-287 should be accepted, block 288 should be ignored because it's too far ahead
for x in all_blocks[:-1]: for x in all_blocks[:-1]:
self.nodes[0].getblock(x.hash) self.nodes[0].getblock(x.hash)
assert_raises_rpc_error(-1, "Block not found on disk", self.nodes[0].getblock, all_blocks[-1].hash) assert_raises_rpc_error(-1, "Block not found on disk", self.nodes[0].getblock, all_blocks[-1].hash)
headers_message.headers.pop() # Ensure the last block is unrequested
white_node.send_message(headers_message) # Send headers leading to tip
white_node.send_message(msg_block(tips[1])) # Now deliver the tip
white_node.sync_with_ping()
self.nodes[1].getblock(tips[1].hash)
self.log.info("Unrequested block far ahead of tip accepted from whitelisted peer")
# 5. Test handling of unrequested block on the node that didn't process # 5. Test handling of unrequested block on the node that didn't process
# Should still not be processed (even though it has a child that has more # Should still not be processed (even though it has a child that has more
# work). # work).
test_node.send_message(msg_block(blocks_h2f[0]))
# Here, if the sleep is too short, the test could falsely succeed (if the # The node should have requested the blocks at some point, so
# node hasn't processed the block by the time the sleep returns, and then # disconnect/reconnect first
# the node processes it and incorrectly advances the tip). connections[0].disconnect_node()
# But this would be caught later on, when we verify that an inv triggers test_node.wait_for_disconnect()
# a getdata request for this block.
test_node = NodeConnCB() # connects to node (not whitelisted)
connections[0] = NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], test_node)
test_node.add_connection(connections[0])
test_node.wait_for_verack()
test_node.send_message(msg_block(block_h1f))
test_node.sync_with_ping() test_node.sync_with_ping()
assert_equal(self.nodes[0].getblockcount(), 2) assert_equal(self.nodes[0].getblockcount(), 2)
self.log.info("Unrequested block that would complete more-work chain was ignored") self.log.info("Unrequested block that would complete more-work chain was ignored")
@ -220,27 +229,99 @@ class AcceptBlockTest(BitcoinTestFramework):
with mininode_lock: with mininode_lock:
# Clear state so we can check the getdata request # Clear state so we can check the getdata request
test_node.last_message.pop("getdata", None) test_node.last_message.pop("getdata", None)
test_node.send_message(msg_inv([CInv(2, blocks_h3[0].sha256)])) test_node.send_message(msg_inv([CInv(2, block_h3.sha256)]))
test_node.sync_with_ping() test_node.sync_with_ping()
with mininode_lock: with mininode_lock:
getdata = test_node.last_message["getdata"] getdata = test_node.last_message["getdata"]
# Check that the getdata includes the right block # Check that the getdata includes the right block
assert_equal(getdata.inv[0].hash, blocks_h2f[0].sha256) assert_equal(getdata.inv[0].hash, block_h1f.sha256)
self.log.info("Inv at tip triggered getdata for unprocessed block") self.log.info("Inv at tip triggered getdata for unprocessed block")
# 7. Send the missing block for the third time (now it is requested) # 7. Send the missing block for the third time (now it is requested)
test_node.send_message(msg_block(blocks_h2f[0])) test_node.send_message(msg_block(block_h1f))
test_node.sync_with_ping() test_node.sync_with_ping()
assert_equal(self.nodes[0].getblockcount(), 290) assert_equal(self.nodes[0].getblockcount(), 290)
self.nodes[0].getblock(all_blocks[286].hash)
assert_equal(self.nodes[0].getbestblockhash(), all_blocks[286].hash)
assert_raises_rpc_error(-1, "Block not found on disk", self.nodes[0].getblock, all_blocks[287].hash)
self.log.info("Successfully reorged to longer chain from non-whitelisted peer") self.log.info("Successfully reorged to longer chain from non-whitelisted peer")
# 8. Connect node2 to node0 and ensure it is able to sync # 8. Create a chain which is invalid at a height longer than the
connect_nodes(self.nodes[0], 2) # current chain, but which has more blocks on top of that
sync_blocks([self.nodes[0], self.nodes[2]]) block_289f = create_block(all_blocks[284].sha256, create_coinbase(289), all_blocks[284].nTime+1)
self.log.info("Successfully synced nodes 2 and 0") block_289f.solve()
block_290f = create_block(block_289f.sha256, create_coinbase(290), block_289f.nTime+1)
block_290f.solve()
block_291 = create_block(block_290f.sha256, create_coinbase(291), block_290f.nTime+1)
# block_291 spends a coinbase below maturity!
block_291.vtx.append(create_transaction(block_290f.vtx[0], 0, b"42", 1))
block_291.hashMerkleRoot = block_291.calc_merkle_root()
block_291.solve()
block_292 = create_block(block_291.sha256, create_coinbase(292), block_291.nTime+1)
block_292.solve()
# Now send all the headers on the chain and enough blocks to trigger reorg
headers_message = msg_headers()
headers_message.headers.append(CBlockHeader(block_289f))
headers_message.headers.append(CBlockHeader(block_290f))
headers_message.headers.append(CBlockHeader(block_291))
headers_message.headers.append(CBlockHeader(block_292))
test_node.send_message(headers_message)
test_node.sync_with_ping()
tip_entry_found = False
for x in self.nodes[0].getchaintips():
if x['hash'] == block_292.hash:
assert_equal(x['status'], "headers-only")
tip_entry_found = True
assert(tip_entry_found)
assert_raises_rpc_error(-1, "Block not found on disk", self.nodes[0].getblock, block_292.hash)
test_node.send_message(msg_block(block_289f))
test_node.send_message(msg_block(block_290f))
test_node.sync_with_ping()
self.nodes[0].getblock(block_289f.hash)
self.nodes[0].getblock(block_290f.hash)
test_node.send_message(msg_block(block_291))
# At this point we've sent an obviously-bogus block, wait for full processing
# without assuming whether we will be disconnected or not
try:
# Only wait a short while so the test doesn't take forever if we do get
# disconnected
test_node.sync_with_ping(timeout=1)
except AssertionError:
test_node.wait_for_disconnect()
test_node = NodeConnCB() # connects to node (not whitelisted)
connections[0] = NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], test_node)
test_node.add_connection(connections[0])
NetworkThread().start() # Start up network handling in another thread
test_node.wait_for_verack()
# We should have failed reorg and switched back to 290 (but have block 291)
assert_equal(self.nodes[0].getblockcount(), 290)
assert_equal(self.nodes[0].getbestblockhash(), all_blocks[286].hash)
assert_equal(self.nodes[0].getblock(block_291.hash)["confirmations"], -1)
# Now send a new header on the invalid chain, indicating we're forked off, and expect to get disconnected
block_293 = create_block(block_292.sha256, create_coinbase(293), block_292.nTime+1)
block_293.solve()
headers_message = msg_headers()
headers_message.headers.append(CBlockHeader(block_293))
test_node.send_message(headers_message)
test_node.wait_for_disconnect()
# 9. Connect node1 to node0 and ensure it is able to sync
connect_nodes(self.nodes[0], 1)
sync_blocks([self.nodes[0], self.nodes[1]])
self.log.info("Successfully synced nodes 1 and 0")
[ c.disconnect_node() for c in connections ] [ c.disconnect_node() for c in connections ]

2
test/functional/test_runner.py

@ -125,6 +125,7 @@ BASE_SCRIPTS= [
'minchainwork.py', 'minchainwork.py',
'p2p-fingerprint.py', 'p2p-fingerprint.py',
'uacomment.py', 'uacomment.py',
'p2p-acceptblock.py',
] ]
EXTENDED_SCRIPTS = [ EXTENDED_SCRIPTS = [
@ -152,7 +153,6 @@ EXTENDED_SCRIPTS = [
'txn_clone.py --mineblock', 'txn_clone.py --mineblock',
'notifications.py', 'notifications.py',
'invalidateblock.py', 'invalidateblock.py',
'p2p-acceptblock.py',
'replace-by-fee.py', 'replace-by-fee.py',
] ]

Loading…
Cancel
Save