diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 98e3d90c2d1e..d71b4f788d2c 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -1430,6 +1430,33 @@ bool static AlreadyHaveBlock(const uint256& block_hash) EXCLUSIVE_LOCKS_REQUIRED return LookupBlockIndex(block_hash) != nullptr; } +bool FetchBlock(const NodeId nodeid, const CBlockIndex* pindex, CConnman& connman, CTxMemPool& mempool) EXCLUSIVE_LOCKS_REQUIRED(cs_main) +{ + PeerRef peer = GetPeerRef(nodeid); + if (peer == nullptr) return false; + uint32_t nFetchFlags = 0; + CNodeState* state = State(nodeid); + if (state == nullptr) { + return false; + } + const int node_sync_height = state->pindexBestKnownBlock ? state->pindexBestKnownBlock->nHeight : -1; + if (pindex->nHeight > node_sync_height) { + return false; + } + if (state->fHaveWitness) { + nFetchFlags |= MSG_WITNESS_FLAG; + } + std::vector vInv(1); + vInv[0] = CInv(MSG_BLOCK | nFetchFlags, pindex->GetBlockHash()); + bool success = connman.ForNode(nodeid, [&connman, vInv](CNode* pnode) { + const CNetMsgMaker msgMaker(pnode->GetCommonVersion()); + connman.PushMessage(pnode, msgMaker.Make(NetMsgType::GETDATA, vInv)); + return true; + }); + MarkBlockAsInFlight(mempool, nodeid, pindex->GetBlockHash(), pindex); + return success; +} + void RelayTransaction(const uint256& txid, const uint256& wtxid, const CConnman& connman) { connman.ForEachNode([&txid, &wtxid](CNode* pnode) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { diff --git a/src/net_processing.h b/src/net_processing.h index 87eee566deb8..556a8a18cb5c 100644 --- a/src/net_processing.h +++ b/src/net_processing.h @@ -152,6 +152,11 @@ struct CNodeStateStats { std::vector vHeightInFlight; }; +/** Attempt to manually fetch block from a given node. + * A refactor of in flight block tracking could remove the need for passing CTxMemPool in + */ +bool FetchBlock(const NodeId nodeid, const CBlockIndex* pindex, CConnman& connman, CTxMemPool& mempool); + /** Get statistics from node state */ bool GetNodeStateStats(NodeId nodeid, CNodeStateStats &stats); diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 392073d04707..4237999c6373 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -14,6 +14,8 @@ #include #include #include +#include // For NodeId +#include #include #include #include @@ -753,6 +755,48 @@ static RPCHelpMan getmempoolentry() }; } +static RPCHelpMan getblockfrompeer() +{ + return RPCHelpMan{"getblockfrompeer", + "\nAttempts to fetch block from a given peer.\n", + { + {"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::NONE, "", ""}, + RPCExamples{ + HelpExampleCli("getblockfrompeer", "\"00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09\" 0") + + HelpExampleRpc("getblockfrompeer", "\"00000000c937983704a73af28acdec37b049d214adbda81d7e2a3dd146f6ed09\" 0") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + uint256 hash(ParseHashV(request.params[0], "hash")); + + const CBlockIndex* pblockindex; + { + LOCK(cs_main); + pblockindex = LookupBlockIndex(hash); + } + + if (!pblockindex) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found"); + } + + const UniValue &id_arg = request.params[1]; + const NodeId nodeid = (NodeId) id_arg.get_int64(); + const NodeContext& node_context = EnsureNodeContext(request.context); + + CTxMemPool& mempool = EnsureMemPool(request.context); + LOCK(mempool.cs); + + if (!FetchBlock(nodeid, pblockindex, *node_context.connman, mempool)) { + throw JSONRPCError(RPC_MISC_ERROR, "Failed to fetch block from peer"); + } + return NullUniValue; +}, + }; +} + static RPCHelpMan getblockhash() { return RPCHelpMan{"getblockhash", @@ -2472,6 +2516,7 @@ static const CRPCCommand commands[] = { "blockchain", "getbestblockhash", &getbestblockhash, {} }, { "blockchain", "getblockcount", &getblockcount, {} }, { "blockchain", "getblock", &getblock, {"blockhash","verbosity|verbose"} }, + { "blockchain", "getblockfrompeer", &getblockfrompeer, {"blockhash","nodeid"} }, { "blockchain", "getblockhash", &getblockhash, {"height"} }, { "blockchain", "getblockheader", &getblockheader, {"blockhash","verbose"} }, { "blockchain", "getchaintips", &getchaintips, {} }, diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 042005b9a6ae..b9ffe7fecfc4 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -56,6 +56,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "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/test/functional/rpc_getblockfrompeer.py b/test/functional/rpc_getblockfrompeer.py new file mode 100755 index 000000000000..54e000a9318e --- /dev/null +++ b/test/functional/rpc_getblockfrompeer.py @@ -0,0 +1,58 @@ +#!/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.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, + assert_raises_rpc_error, +) + +import time + + +class GetBlockFromPeerTest(BitcoinTestFramework): + def set_test_params(self): + self.num_nodes = 2 + + def setup_network(self): + self.setup_nodes() + + def run_test(self): + self.log.info("Mine 4 blocks on Node 0") + self.nodes[0].generatetoaddress(4, self.nodes[0].get_deterministic_priv_key().address) + assert_equal(self.nodes[0].getblockcount(), 204) + + self.log.info("Mine competing 3 blocks on Node 1") + self.nodes[1].generatetoaddress(3, self.nodes[1].get_deterministic_priv_key().address) + 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.nodes[0:2]) + + self.log.info("Node 0 should only have the header for node 1's block 3") + short_tip_synced = False + for x in self.nodes[0].getchaintips(): + if x['hash'] == short_tip: + assert_equal(x['status'], "headers-only") + short_tip_synced = True + assert short_tip_synced + 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.nodes[0].getblockfrompeer(short_tip, peer_0_peer_1_id) + time.sleep(1) + self.nodes[0].getblock(short_tip) + + 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) + +if __name__ == '__main__': + GetBlockFromPeerTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 5b3db282e15a..0ad08c93f975 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -201,6 +201,7 @@ 'wallet_txn_clone.py --mineblock', 'feature_notifications.py', 'rpc_getblockfilter.py', + 'rpc_getblockfrompeer.py', 'rpc_invalidateblock.py', 'feature_rbf.py', 'mempool_packages.py',