diff --git a/src/net_processing.cpp b/src/net_processing.cpp --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -917,7 +917,9 @@ LOCK(cs_main); static int nHighestFastAnnounce = 0; - if (pindex->nHeight <= nHighestFastAnnounce) { + // pindex->nHeight == nHighestFastAnnounce is expected when a more + // honest-looking block replaces a selfishly-mined block. + if (pindex->nHeight < nHighestFastAnnounce) { return; } nHighestFastAnnounce = pindex->nHeight; diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -3875,8 +3875,10 @@ } bool fHasMoreWork = - (chainActive.Tip() ? pindex->nChainWork > chainActive.Tip()->nChainWork - : true); + (chainActive.Tip() + ? (pindex->nChainWork > chainActive.Tip()->nChainWork) || + isSameHeightAndMoreHonestlyMined + : true); // Blocks that are too out-of-order needlessly limit the effectiveness of // pruning, because pruning will not delete block files that contain any @@ -3933,7 +3935,7 @@ // Header is valid/has work and the merkle tree is good. // Relay now, but if it does not build on our best tip, let the // SendMessages loop relay it. - if (!IsInitialBlockDownload() && chainActive.Tip() == pindex->pprev) { + if (!IsInitialBlockDownload() && fHasMoreWork) { GetMainSignals().NewPoWValidBlock(pindex, pblock); } @@ -3964,6 +3966,14 @@ return AbortNode(state, std::string("System error: ") + e.what()); } + if (fHasMoreWork && isSameHeightAndMoreHonestlyMined) { + if (PreciousBlock(config, state, pindex)) { + LogPrintf("More honestly broadcasted block has replaced chain tip " + "(set as precious block): %s\n", + pindex->GetBlockHash().ToString()); + } + } + if (fCheckForPruning) { // we just allocated more disk space for block files. FlushStateToDisk(config.GetChainParams(), state, FLUSH_STATE_NONE); diff --git a/test/functional/sendheaders-selfish-mining.py b/test/functional/sendheaders-selfish-mining.py new file mode 100755 --- /dev/null +++ b/test/functional/sendheaders-selfish-mining.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# Copyright (c) 2018 The Bitcoin ABC developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +# TODO DOCSTRING +"""An example functional test + +The module-level docstring should include a high-level description of +what the test is doing. It's the first thing people see when they open +the file and should give the reader information about *what* the test +is testing and *how* it's being tested +""" +# Imports should be in PEP8 ordering (std library first, then third party +# libraries then local imports). +from collections import defaultdict +import time + +# Avoid wildcard * imports if possible +from test_framework.blocktools import (create_block, create_coinbase) +from test_framework.mininode import ( + NetworkThread, + NodeConn, + NodeConnCB, + msg_block, +) +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + p2p_port, + wait_until, +) + + +class DissuadeSelfishMiningTests(BitcoinTestFramework): + + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 2 + + def get_chaintip_hashes(self, node): + hashes = [] + for tip in node.getchaintips(): + hashes.append(tip['hash']) + return hashes + + def run_test(self): + node = NodeConnCB() + connections = [] + connections.append( + NodeConn('127.0.0.1', p2p_port(0), self.nodes[0], node)) + node.add_connection(connections[0]) + + # Start up network handling in another thread. This needs to be called + # after the P2P connections have been created. + NetworkThread().start() + # wait_for_verack ensures that the P2P connection is fully up. + node.wait_for_verack() + + blocks = [int(self.nodes[0].generate(nblocks=1)[0], 16)] + self.sync_all([self.nodes[0:1]]) + + # A selfish miner sending out headers in response to an honest block + # will not get the block relayed, as the honest block was seen first. + common_block = int(self.nodes[0].getbestblockhash(), 16) + block_time = self.nodes[0].getblock( + self.nodes[0].getbestblockhash())['time'] + 1 + height = self.nodes[0].getblockcount() + 1 + + selfish_block1 = create_block( + common_block, create_coinbase(height), block_time) + selfish_block1.solve() + + block_time += 1 + honest_block1 = create_block( + common_block, create_coinbase(height), block_time) + honest_block1.solve() + + # Selfishly mined block gets broadcasted immediately after the honest block + node.send_message(msg_block(honest_block1)) + node.send_and_ping(msg_block(selfish_block1)) + + actual_tip = int(self.nodes[0].getbestblockhash(), 16) + + # Node0 should have both blocks as chaintips, with the honest block as the active tip + assert(honest_block1.hash in self.get_chaintip_hashes(self.nodes[0])) + assert(selfish_block1.hash in self.get_chaintip_hashes(self.nodes[0])) + assert_equal(actual_tip, honest_block1.sha256) + + common_block = actual_tip + block_time += 1 + height += 1 + + # A selfish miner withholding a block for too long will allow it to be replaced + # as the active chain tip by an honestly-mined block that was received at a more + # accurate time compared to the block timestamp. + selfish_block2 = create_block( + common_block, create_coinbase(height), block_time) + selfish_block2.solve() + + block_time += 20 + honest_block2 = create_block( + common_block, create_coinbase(height), block_time) + honest_block2.solve() + + # Wait for enough time to pass so that the selfish block is identified (>10s) + time.sleep(20) + node.send_and_ping(msg_block(selfish_block2)) + node.send_and_ping(msg_block(honest_block2)) + + actual_tip = int(self.nodes[1].getbestblockhash(), 16) + # selfish block is no longer the active tip + assert_equal(actual_tip, honest_block2.sha256) + + +if __name__ == '__main__': + DissuadeSelfishMiningTests().main()