diff --git a/src/chain.h b/src/chain.h --- a/src/chain.h +++ b/src/chain.h @@ -30,6 +30,15 @@ */ static const int64_t TIMESTAMP_WINDOW = MAX_FUTURE_BLOCK_TIME; +/** + * This threshold is used to determine tie-breaks between competing blocks + * at the same height. A selfishly-mined block will likely have a larger + * header timestamp to received time difference. This threshold is the margin + * of error that a header may have to still be considered honest when compared + * to another block. + */ +static const int64_t SELFISH_MINER_HEADER_RECEIVE_TIME_THRESHOLD = 10; + class CBlockFileInfo { public: //!< number of blocks stored in file @@ -233,6 +242,9 @@ uint32_t nBits; uint32_t nNonce; + //! (memory only) block header metadata + uint64_t nTimeReceived; + //! (memory only) Sequential id assigned to distinguish order in which //! blocks are received. int32_t nSequenceId; @@ -258,6 +270,7 @@ nVersion = 0; hashMerkleRoot = uint256(); nTime = 0; + nTimeReceived = 0; nBits = 0; nNonce = 0; } @@ -270,6 +283,7 @@ nVersion = block.nVersion; hashMerkleRoot = block.hashMerkleRoot; nTime = block.nTime; + nTimeReceived = block.nTimeReceived; nBits = block.nBits; nNonce = block.nNonce; } @@ -300,6 +314,7 @@ } block.hashMerkleRoot = hashMerkleRoot; block.nTime = nTime; + block.nTimeReceived = nTimeReceived; block.nBits = nBits; block.nNonce = nNonce; return block; @@ -311,6 +326,10 @@ int64_t GetBlockTimeMax() const { return int64_t(nTimeMax); } + int64_t GetHeaderTimeReceived() const { + return nTimeReceived ? nTimeReceived : GetBlockTime(); + } + enum { nMedianTimeSpan = 11 }; int64_t GetMedianTimePast() const { diff --git a/src/net_processing.cpp b/src/net_processing.cpp --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -8,6 +8,7 @@ #include "addrman.h" #include "arith_uint256.h" #include "blockencodings.h" +#include "chain.h" #include "chainparams.h" #include "config.h" #include "consensus/validation.h" @@ -917,7 +918,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; @@ -2255,6 +2258,9 @@ CBlockHeaderAndShortTxIDs cmpctblock; vRecv >> cmpctblock; + int64_t receiveTime = GetTime(); + cmpctblock.header.nTimeReceived = receiveTime; + { LOCK(cs_main); @@ -2616,6 +2622,11 @@ return true; } + int64_t receiveTime = GetTime(); + for (CBlockHeader &header : headers) { + header.nTimeReceived = receiveTime; + } + const CBlockIndex *pindexLast = nullptr; { LOCK(cs_main); @@ -2719,8 +2730,9 @@ bool fCanDirectFetch = CanDirectFetch(chainparams.GetConsensus()); // If this set of headers is valid and ends in a block with at least // as much work as our tip, download as much as possible. - if (fCanDirectFetch && pindexLast->IsValid(BLOCK_VALID_TREE) && - chainActive.Tip()->nChainWork <= pindexLast->nChainWork) { + if (fCanDirectFetch && + pindexLast->IsValid(BLOCK_VALID_TREE) && + chainActive.Tip()->nChainWork <= pindexLast->nChainWork) { std::vector vToFetch; const CBlockIndex *pindexWalk = pindexLast; // Calculate all the blocks we'd need to switch to pindexLast, @@ -2796,6 +2808,9 @@ std::shared_ptr pblock = std::make_shared(); vRecv >> *pblock; + int64_t receiveTime = GetTime(); + pblock->nTimeReceived = receiveTime; + LogPrint(BCLog::NET, "received block %s peer=%d\n", pblock->GetHash().ToString(), pfrom->id); diff --git a/src/primitives/block.h b/src/primitives/block.h --- a/src/primitives/block.h +++ b/src/primitives/block.h @@ -28,6 +28,9 @@ uint32_t nBits; uint32_t nNonce; + // local header metadata (never include these in CBlockHeader serialization) + uint64_t nTimeReceived; // microseconds + CBlockHeader() { SetNull(); } ADD_SERIALIZE_METHODS; @@ -47,6 +50,7 @@ hashPrevBlock.SetNull(); hashMerkleRoot.SetNull(); nTime = 0; + nTimeReceived = 0; nBits = 0; nNonce = 0; } @@ -93,6 +97,7 @@ block.hashPrevBlock = hashPrevBlock; block.hashMerkleRoot = hashMerkleRoot; block.nTime = nTime; + block.nTimeReceived = nTimeReceived; block.nBits = nBits; block.nNonce = nNonce; return block; diff --git a/src/util.h b/src/util.h --- a/src/util.h +++ b/src/util.h @@ -95,6 +95,7 @@ COINDB = (1 << 18), QT = (1 << 19), LEVELDB = (1 << 20), + HEADERTIMESTAMPS = (1 << 21), ALL = ~uint32_t(0), }; } diff --git a/src/util.cpp b/src/util.cpp --- a/src/util.cpp +++ b/src/util.cpp @@ -243,6 +243,7 @@ {BCLog::COINDB, "coindb"}, {BCLog::QT, "qt"}, {BCLog::LEVELDB, "leveldb"}, + {BCLog::HEADERTIMESTAMPS, "headertimestamps"}, {BCLog::ALL, "1"}, {BCLog::ALL, "all"}, }; diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -3740,9 +3740,23 @@ // process an unrequested block if it's new and has enough work to // advance our tip, and isn't too many blocks ahead. bool fAlreadyHave = pindex->nStatus & BLOCK_HAVE_DATA; + + // Compare block header timestamps and received times of the block and the chaintip. + // If they have the same chain height, use these diffs as a tie-breaker, attempting + // to pick the more honestly-mined block. + int64_t newBlockTimeDiff = pindex->GetHeaderTimeReceived() - pindex->GetBlockTime(); + int64_t chainTipTimeDiff = chainActive.Tip()->GetHeaderTimeReceived() - chainActive.Tip()->GetBlockTime(); + + bool isSameHeightAndMoreHonestlyMined = (pindex->nChainWork == chainActive.Tip()->nChainWork) && (newBlockTimeDiff + SELFISH_MINER_HEADER_RECEIVE_TIME_THRESHOLD < chainTipTimeDiff); + if (isSameHeightAndMoreHonestlyMined) { + LogPrint(BCLog::HEADERTIMESTAMPS, "Chain tip timestamp-to-received-time difference: hash=%s, diff=%d\n", chainActive.Tip()->GetBlockHash().ToString(), chainTipTimeDiff); + LogPrint(BCLog::HEADERTIMESTAMPS, "New block timestamp-to-received-time difference: hash=%s, diff=%d\n", pindex->GetBlockHash().ToString(), newBlockTimeDiff); + } + bool fHasMoreWork = - (chainActive.Tip() ? pindex->nChainWork > chainActive.Tip()->nChainWork + (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 // blocks which are too close in height to the tip. Apply this test @@ -3798,7 +3812,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); } @@ -3829,6 +3843,12 @@ return AbortNode(state, std::string("System error: ") + e.what()); } + if (fHasMoreWork && isSameHeightAndMoreHonestlyMined) { + if (PreciousBlock(config, state, pindex)) { + LogPrint(BCLog::HEADERTIMESTAMPS, "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()