diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -7,3 +7,10 @@ of the release shipment. Users that are willing to build for 32 bits Windows should be aware that this will not be tested by the Bitcoin ABC team and be prepared to face issues. + +Seeder +------ + - Seeder now requires nodes to have the block after the most recent checkpoint + in its chain in order to be considered a good seed. + - Nodes that are undergoing IBD are not considered good seeds until their + chain reaches the block after the most recent checkpoint. diff --git a/src/seeder/bitcoin.h b/src/seeder/bitcoin.h --- a/src/seeder/bitcoin.h +++ b/src/seeder/bitcoin.h @@ -16,6 +16,7 @@ return Params().GetDefaultPort(); } +static const unsigned int MAX_HEADERS_PER_MESSAGE = 2000; static const unsigned int MAX_ADDRS_PER_MESSAGE = 1000; // The network magic to use. @@ -53,6 +54,8 @@ void PushVersion(); + void RequestHeaders(); + bool ProcessMessages(); protected: diff --git a/src/seeder/bitcoin.cpp b/src/seeder/bitcoin.cpp --- a/src/seeder/bitcoin.cpp +++ b/src/seeder/bitcoin.cpp @@ -90,6 +90,24 @@ EndMessage(); } +void CSeederNode::RequestHeaders() { + // Create a getheaders message + BeginMessage("getheaders"); + // Locator hash: Seeder requests the headers after the latest checkpoint + const CChainParams &chainParams = Params(); + const auto checkpoints = chainParams.Checkpoints().mapCheckpoints; + std::vector locatorHashes; + for (auto checkpoint = checkpoints.rbegin(); + checkpoint != checkpoints.rend(); checkpoint++) { + locatorHashes.push_back(checkpoint->second); + } + locatorHashes.push_back(chainParams.GenesisBlock().GetHash()); + vSend << PROTOCOL_VERSION << locatorHashes << uint256(); + EndMessage(); + // Update socket timeout + doneAfter = time(nullptr) + GetTimeout(); +} + PeerMessagingState CSeederNode::ProcessMessage(std::string strCommand, CDataStream &recv) { // fprintf(stdout, "%s: RECV %s\n", ToString(you).c_str(), @@ -120,10 +138,8 @@ BeginMessage("getaddr"); EndMessage(); doneAfter = time(nullptr) + GetTimeout(); - } else { - doneAfter = time(nullptr) + 1; + return PeerMessagingState::AwaitingMessages; } - return PeerMessagingState::AwaitingMessages; } if (strCommand == "addr" && vAddr) { @@ -154,13 +170,64 @@ // ToString(you).c_str(), // addr.ToString().c_str(), (int)(vAddr->size())); if (vAddr->size() > MAX_ADDRS_PER_MESSAGE) { - doneAfter = 1; - return PeerMessagingState::Finished; + break; } } - return PeerMessagingState::AwaitingMessages; } + const CChainParams &chainParams = Params(); + if (strCommand == NetMsgType::HEADERS) { + uint64_t nHeaders = ReadCompactSize(recv); + // Peer node did not have the checkpoint block requested by the seeder + // Peer only has up to the checkpoint or only has the genesis block. + // Since there is no way to tell the difference mark the peer as bad, + // but don't ban. + if (nHeaders == 0) { + doneAfter = 0; + return PeerMessagingState::Finished; + } + // Peer node sent a malformed headers message + if (nHeaders > MAX_HEADERS_PER_MESSAGE) { + ban = 100000; + return PeerMessagingState::Finished; + } + CBlockHeader header; + recv >> header; + /** + * If the node has this checkpoint then it will return the first + * MAX_HEADERS_PER_MESSAGE block headers it knows that come after the + * checkpoint. This means the hashPrevBlock of the first header + * received should be the hash of the checkpoint if the node is + * following the correct chain. This type of node will be marked good. + */ + if (chainParams.Checkpoints().mapCheckpoints.rbegin()->second == + header.hashPrevBlock) { + // A node is good if it replies with addrs and it is following the + // correct chain + doneAfter = 1; + return PeerMessagingState::Finished; + } + + // The node does not have the checkpoint and therefore the hashPrevBlock + // of the first header will be the genesis hash. + if (chainParams.GenesisBlock().GetHash() == header.hashPrevBlock) { + doneAfter = 0; + return PeerMessagingState::Finished; + } + + /** + * To reach here, the connected node would have to reply with a garbage + * message (ex a message containing improperly sorted headers) which + * would mean it is misbehaving on some level. This node should be + * banned. + */ + ban = 100000; + return PeerMessagingState::Finished; + } + + if (strCommand == "addr" || strCommand == "verack") { + RequestHeaders(); + } return PeerMessagingState::AwaitingMessages; } diff --git a/src/seeder/test/p2p_messaging_tests.cpp b/src/seeder/test/p2p_messaging_tests.cpp --- a/src/seeder/test/p2p_messaging_tests.cpp +++ b/src/seeder/test/p2p_messaging_tests.cpp @@ -5,6 +5,7 @@ #define BOOST_TEST_MODULE Bitcoin Seeder Test Suite #include +#include #include #include #include @@ -32,7 +33,7 @@ public: TestCSeederNode(const CService &service, std::vector *vAddrIn) : CSeederNode(service, vAddrIn) { - SelectParams(CBaseChainParams::REGTEST); + SelectParams(CBaseChainParams::MAIN); } void TestProcessMessage(const std::string &strCommand, CDataStream &message, @@ -119,7 +120,7 @@ sendAddrs.resize(1); addrMessage = CreateAddrMessage(sendAddrs); testNode->TestProcessMessage(NetMsgType::ADDR, addrMessage, - PeerMessagingState::Finished); + PeerMessagingState::AwaitingMessages); BOOST_CHECK_EQUAL(MAX_ADDRS_PER_MESSAGE + 1, vAddr.size()); // Test the seeder's behavior after MAX_ADDRS_PER_MESSAGE addrs @@ -129,10 +130,104 @@ sendAddrs.resize(i, sendAddrs[0]); addrMessage = CreateAddrMessage(sendAddrs); testNode->TestProcessMessage(NetMsgType::ADDR, addrMessage, - PeerMessagingState::Finished); + PeerMessagingState::AwaitingMessages); BOOST_CHECK_EQUAL(expectedSize, vAddr.size()); ++expectedSize; } } +enum class HeaderMessageType { + WRONG_CHAIN_HEADER, + GOOD_HEADER, + NO_HEADERS, + TOO_MANY_HEADERS, + IBD, +}; + +static CDataStream CreateHeadersMessage(HeaderMessageType headersMessageContent, + int32_t nVersion = INIT_PROTO_VERSION, + uint32_t timeStamp = 0, + uint32_t bits = 0, uint32_t nonce = 0) { + CDataStream payload(SER_NETWORK, nVersion); + if (headersMessageContent == HeaderMessageType::NO_HEADERS) { + payload << 0; + return payload; + } + uint16_t numberOfHeaders = 1; + if (headersMessageContent == HeaderMessageType::TOO_MANY_HEADERS) { + numberOfHeaders = MAX_HEADERS_PER_MESSAGE + 1; + } + // The actual header can be garbage data, Seeder only cares that it got + // a specific type of message, the size, and the hash of the previous block. + uint256 garbageHash = Hash(payload.begin(), payload.end()); + uint256 merkleRoot = garbageHash; + const CChainParams &chainParams = Params(); + BlockHash previousBlockHash = + chainParams.Checkpoints().mapCheckpoints.rbegin()->second; + if (headersMessageContent == HeaderMessageType::WRONG_CHAIN_HEADER) { + previousBlockHash = BlockHash(garbageHash); + } else if (headersMessageContent == HeaderMessageType::IBD) { + previousBlockHash = + chainParams.GenesisBlock().GetBlockHeader().GetHash(); + } + + CBlockHeader header; + header.nVersion = nVersion; + header.hashPrevBlock = previousBlockHash; + header.hashMerkleRoot = merkleRoot; + header.nTime = timeStamp; + header.nBits = bits; + header.nNonce = nonce; + // we must use CBlocks, as CBlockHeaders won't include the 0x00 nTx + // count at the end + std::vector headers(numberOfHeaders, CBlock(header)); + payload << headers; + return payload; +} + +BOOST_AUTO_TEST_CASE(seeder_node_headers_test_happy_path) { + // Happy path + CDataStream headersMessage = + CreateHeadersMessage(HeaderMessageType::GOOD_HEADER); + testNode->TestProcessMessage(NetMsgType::HEADERS, headersMessage, + PeerMessagingState::Finished); + BOOST_CHECK_EQUAL(testNode->GetBan(), 0); +} + +BOOST_AUTO_TEST_CASE(seeder_node_headers_wrong_chain_test) { + // Seeder receives a HEADERS message following the wrong chain + CDataStream headersMessage = + CreateHeadersMessage(HeaderMessageType::WRONG_CHAIN_HEADER); + testNode->TestProcessMessage(NetMsgType::HEADERS, headersMessage, + PeerMessagingState::Finished); + BOOST_CHECK_EQUAL(testNode->GetBan(), 100000); +} + +BOOST_AUTO_TEST_CASE(seeder_node_empty_headers_test) { + // Seeder receives an empty HEADERS message + CDataStream headersMessage = + CreateHeadersMessage(HeaderMessageType::NO_HEADERS); + testNode->TestProcessMessage(NetMsgType::HEADERS, headersMessage, + PeerMessagingState::Finished); + BOOST_CHECK_EQUAL(testNode->GetBan(), 0); +} + +BOOST_AUTO_TEST_CASE(seeder_node_too_many_headers_test) { + // Seeder receives a HEADERS message with too many headers + CDataStream headersMessage = + CreateHeadersMessage(HeaderMessageType::TOO_MANY_HEADERS); + testNode->TestProcessMessage(NetMsgType::HEADERS, headersMessage, + PeerMessagingState::Finished); + BOOST_CHECK_EQUAL(testNode->GetBan(), 100000); +} + +BOOST_AUTO_TEST_CASE(seeder_node_ibd_headers_test) { + // Seeder receives a HEADERS message with the first header belonging to the + // block that comes after the genesis block + CDataStream headersMessage = CreateHeadersMessage(HeaderMessageType::IBD); + testNode->TestProcessMessage(NetMsgType::HEADERS, headersMessage, + PeerMessagingState::Finished); + BOOST_CHECK_EQUAL(testNode->GetBan(), 0); +} + BOOST_AUTO_TEST_SUITE_END()