diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -5,3 +5,6 @@ This release includes the following features and fixes: + - Add a new RPC `getblockfrompeer` which permits requesting a specific block from + a specific peer manually. The intended use is acquisition of stale chaintips + for fork monitoring and research purposes. diff --git a/src/net_processing.h b/src/net_processing.h --- a/src/net_processing.h +++ b/src/net_processing.h @@ -56,6 +56,18 @@ bool ignore_incoming_txs); virtual ~PeerManager() {} + /** + * Attempt to manually fetch block from a given peer. We must already have + * the header. + * + * @param[in] config The global config + * @param[in] id The peer id + * @param[in] block_index The block index + * @returns Whether a request was successfully made + */ + virtual bool FetchBlock(const Config &config, NodeId id, + const CBlockIndex &block_index) = 0; + /** Begin running background tasks, should only be called once */ virtual void StartScheduledTasks(CScheduler &scheduler) = 0; diff --git a/src/net_processing.cpp b/src/net_processing.cpp --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -494,6 +494,8 @@ /** Implement PeerManager */ void StartScheduledTasks(CScheduler &scheduler) override; void CheckForStaleTipAndEvictPeers() override; + bool FetchBlock(const Config &config, NodeId id, + const CBlockIndex &block_index) override; bool GetNodeStateStats(NodeId nodeid, CNodeStateStats &stats) const override; bool IgnoresIncomingTxs() override { return m_ignore_incoming_txs; } @@ -2059,6 +2061,47 @@ STALE_RELAY_AGE_LIMIT); } +bool PeerManagerImpl::FetchBlock(const Config &config, NodeId id, + const CBlockIndex &block_index) { + if (fImporting || fReindex) { + return false; + } + + LOCK(cs_main); + // Ensure this peer exists and hasn't been disconnected + CNodeState *state = State(id); + if (state == nullptr) { + return false; + } + // Mark block as in-flight unless it already is (for this peer). + // If a block was already in-flight for a different peer, its BLOCKTXN + // response will be dropped. + if (!BlockRequested(config, id, block_index)) { + return false; + } + + // Construct message to request the block + const BlockHash &hash{block_index.GetBlockHash()}; + const std::vector invs{CInv(MSG_BLOCK, hash)}; + + // Send block request message to the peer + if (!m_connman.ForNode(id, [this, &invs](CNode *node) { + const CNetMsgMaker msgMaker(node->GetCommonVersion()); + this->m_connman.PushMessage( + node, msgMaker.Make(NetMsgType::GETDATA, invs)); + return true; + })) { + RemoveBlockRequest(hash); + LogPrint(BCLog::NET, "Failed to request block %s from peer=%d\n", + hash.ToString(), id); + return false; + } + + LogPrint(BCLog::NET, "Requesting block %s from peer=%d\n", hash.ToString(), + id); + return true; +} + std::unique_ptr PeerManager::make(const CChainParams &chainparams, CConnman &connman, AddrMan &addrman, BanMan *banman, ChainstateManager &chainman, diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include #include #include @@ -875,6 +877,69 @@ }; } +static RPCHelpMan getblockfrompeer() { + return RPCHelpMan{ + "getblockfrompeer", + "\nAttempt to fetch block from a given peer.\n" + "\nWe must have the header for this block, e.g. using submitheader.\n" + "Subsequent calls for the same block and a new peer will cause the " + "response from the previous peer to be ignored.\n" + "\nReturns an empty JSON object if the request was successfully " + "scheduled.", + { + {"blockhash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, + "The block hash"}, + {"nodeid", RPCArg::Type::NUM, RPCArg::Optional::NO, + "The node ID (see getpeerinfo for node IDs)"}, + }, + RPCResult{RPCResult::Type::OBJ, + "", + "", + {{RPCResult::Type::STR, "warnings", /*optional=*/true, + "any warnings"}}}, + RPCExamples{HelpExampleCli("getblockfrompeer", + "\"00000000c937983704a73af28acdec37b049d214a" + "dbda81d7e2a3dd146f6ed09\" 0") + + HelpExampleRpc("getblockfrompeer", + "\"00000000c937983704a73af28acdec37b049d214a" + "dbda81d7e2a3dd146f6ed09\" 0")}, + [&](const RPCHelpMan &self, const Config &config, + const JSONRPCRequest &request) -> UniValue { + const NodeContext &node = EnsureAnyNodeContext(request.context); + ChainstateManager &chainman = EnsureChainman(node); + PeerManager &peerman = EnsurePeerman(node); + CConnman &connman = EnsureConnman(node); + + const BlockHash hash{ParseHashV(request.params[0], "hash")}; + const NodeId nodeid{request.params[1].get_int64()}; + + // Check that the peer with nodeid exists + if (!connman.ForNode(nodeid, [](CNode *node) { return true; })) { + throw JSONRPCError( + RPC_MISC_ERROR, + strprintf("Peer nodeid %d does not exist", nodeid)); + } + + const CBlockIndex *const index = WITH_LOCK( + cs_main, return chainman.m_blockman.LookupBlockIndex(hash);); + + if (!index) { + throw JSONRPCError(RPC_MISC_ERROR, "Block header missing"); + } + + UniValue result = UniValue::VOBJ; + + if (index->nStatus.hasData()) { + result.pushKV("warnings", "Block already downloaded"); + } else if (!peerman.FetchBlock(config, nodeid, *index)) { + throw JSONRPCError(RPC_MISC_ERROR, + "Failed to fetch block from peer"); + } + return result; + }, + }; +} + static RPCHelpMan getblockhash() { return RPCHelpMan{ "getblockhash", @@ -3309,6 +3374,7 @@ // ------------------ ---------------------- { "blockchain", getbestblockhash, }, { "blockchain", getblock, }, + { "blockchain", getblockfrompeer, }, { "blockchain", getblockchaininfo, }, { "blockchain", getblockcount, }, { "blockchain", getblockhash, }, diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -49,6 +49,7 @@ {"getbalance", 1, "minconf"}, {"getbalance", 2, "include_watchonly"}, {"getbalance", 3, "avoid_reuse"}, + {"getblockfrompeer", 1, "nodeid"}, {"getblockhash", 0, "height"}, {"waitforblockheight", 0, "height"}, {"waitforblockheight", 1, "timeout"}, diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -4340,6 +4340,7 @@ // This requires some new chain data structure to efficiently look up if a // block is in a chain leading to a candidate for best tip, despite not // being such a candidate itself. + // Note that this would break the getblockfrompeer RPC // If we didn't ask for it: if (!fRequested) { diff --git a/test/functional/rpc_getblockfrompeer.py b/test/functional/rpc_getblockfrompeer.py new file mode 100755 --- /dev/null +++ b/test/functional/rpc_getblockfrompeer.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# Copyright (c) 2020 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test the getblockfrompeer RPC.""" + +from test_framework.authproxy import JSONRPCException +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, assert_raises_rpc_error + + +class GetBlockFromPeerTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 2 + self.extra_args = [[], ["-noparkdeepreorg"]] + + def setup_network(self): + self.setup_nodes() + # Don't connect the nodes initially + + def check_for_block(self, hash): + try: + self.nodes[0].getblock(hash) + return True + except JSONRPCException: + return False + + def run_test(self): + self.log.info("Mine 4 blocks on Node 0") + self.generate(self.nodes[0], 4, sync_fun=self.no_op) + assert_equal(self.nodes[0].getblockcount(), 204) + + self.log.info("Mine competing 3 blocks on Node 1") + self.generate(self.nodes[1], 3, sync_fun=self.no_op) + assert_equal(self.nodes[1].getblockcount(), 203) + short_tip = self.nodes[1].getbestblockhash() + + self.log.info("Connect nodes to sync headers") + self.connect_nodes(0, 1) + self.sync_blocks() + + self.log.info( + "Node 0 should only have the header for node 1's block 3") + x = next( + filter( + lambda x: x['hash'] == short_tip, + self.nodes[0].getchaintips())) + assert_equal(x['status'], "headers-only") + assert_raises_rpc_error(-1, + "Block not found on disk", + self.nodes[0].getblock, + short_tip) + + self.log.info("Fetch block from node 1") + peers = self.nodes[0].getpeerinfo() + assert_equal(len(peers), 1) + peer_0_peer_1_id = peers[0]["id"] + + self.log.info("Arguments must be sensible") + assert_raises_rpc_error(-8, + "hash must be of length 64 (not 4, for '1234')", + self.nodes[0].getblockfrompeer, + "1234", + 0) + + self.log.info("We must already have the header") + assert_raises_rpc_error(-1, + "Block header missing", + self.nodes[0].getblockfrompeer, + "00" * 32, + 0) + + self.log.info("Non-existent peer generates error") + assert_raises_rpc_error(-1, + f"Peer nodeid {peer_0_peer_1_id + 1} does not exist", + self.nodes[0].getblockfrompeer, + short_tip, + peer_0_peer_1_id + 1) + + self.log.info("Successful fetch") + result = self.nodes[0].getblockfrompeer(short_tip, peer_0_peer_1_id) + self.wait_until(lambda: self.check_for_block(short_tip), timeout=1) + assert "warnings" not in result + + self.log.info("Don't fetch blocks we already have") + result = self.nodes[0].getblockfrompeer(short_tip, peer_0_peer_1_id) + assert "warnings" in result + assert_equal(result["warnings"], "Block already downloaded") + + +if __name__ == '__main__': + GetBlockFromPeerTest().main()