diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -10,3 +10,10 @@ - See the release notes from v0.20.6 for a full description of the changes from the 'account' API to the 'label' API. + +Seeder +------ + - Seeder now requires nodes to have the most recent checkpoint in its chain + in order to be considered a good seed. + - Nodes that do not have the most recent checkpoint in its chain are + temporally banned. diff --git a/src/seeder/bitcoin.cpp b/src/seeder/bitcoin.cpp --- a/src/seeder/bitcoin.cpp +++ b/src/seeder/bitcoin.cpp @@ -113,18 +113,61 @@ if (strCommand == "verack") { vRecv.SetVersion(std::min(nVersion, PROTOCOL_VERSION)); - // fprintf(stdout, "\n%s: version %i\n", ToString(you).c_str(), - // nVersion); - if (vAddr) { - BeginMessage("getaddr"); - EndMessage(); - doneAfter = time(nullptr) + GetTimeout(); - } else { - doneAfter = time(nullptr) + 1; - } + // Create a getheaders message + BeginMessage("getheaders"); + // Locator hash; Seeder requests the headers after the latest checkpoint + BlockHash seederCheckpoint = + Params().Checkpoints().mapCheckpoints.rbegin()->second; + std::vector hashStop(32, 0); + uint8_t numberOfLocatorHashes = 1; + vSend << PROTOCOL_VERSION << numberOfLocatorHashes << seederCheckpoint + << uint256(hashStop); + EndMessage(); + // Update socket timeout + doneAfter = time(nullptr) + GetTimeout(); return false; } + if (strCommand == NetMsgType::HEADERS) { + uint64_t nHeaders = ReadCompactSize(recv); + // Peer node did not find the checkpoint requested by the seeder + if (nHeaders == 0) { + ban = 100000; + return true; + } + // Peer node sent a malformed headers message + if (nHeaders > 2000) { + ban = 100000; + return true; + } + CBlockHeader header; + recv >> header; + /** + * If the node has this checkpoint then it will return the first 2000 + * 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. + */ + if (Params().Checkpoints().mapCheckpoints.rbegin()->second == + header.hashPrevBlock) { + if (vAddr) { + BeginMessage("getaddr"); + EndMessage(); + doneAfter = time(nullptr) + GetTimeout(); + } else { + doneAfter = time(nullptr) + 1; + } + return false; + } + + /** + * Conversely, if hashPrevBlock is not the checkpoint, then the node is + * not following the correct chain and should be banned. + */ + ban = 100000; + return true; + } + if (strCommand == "addr" && vAddr) { std::vector vAddrNew; recv >> vAddrNew; 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 @@ -136,4 +137,94 @@ } } +enum HeaderMessageType : uint8_t { + WRONG_CHAIN_HEADER = 0, + GOOD_HEADER = 1, + NO_HEADERS = 2, + TOO_MANY_HEADERS = 3, +}; + +static const uint8_t HEADERS_TXN_COUNT = 0; + +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, 0); + payload.SetVersion(nVersion); + if (headersMessageContent == HeaderMessageType::NO_HEADERS) { + payload << 0; + return payload; + } + if (headersMessageContent == HeaderMessageType::TOO_MANY_HEADERS) { + uint16_t numberOfHeaders = 2001; + WriteCompactSize(payload, numberOfHeaders); + } else { + uint8_t numberOfHeaders = 1; + WriteCompactSize(payload, numberOfHeaders); + } + // 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. + // It also doesn't care about the headers after the first, so we don't have + // to actually fill the message with more than 1 header + uint256 merkleRoot = Hash(payload.begin(), payload.end()); + BlockHash previousBlockHash; + if (headersMessageContent == HeaderMessageType::WRONG_CHAIN_HEADER) { + previousBlockHash = BlockHash(Hash(payload.begin(), payload.end())); + } else { + previousBlockHash = + Params().Checkpoints().mapCheckpoints.rbegin()->second; + } + payload << nVersion; + // prev_block + payload << previousBlockHash; + // merkle_root + payload << merkleRoot; + // timestamp + payload << timeStamp; + // bits + payload << bits; + // nonce + payload << nonce; + // txn_count + payload << HEADERS_TXN_COUNT; + 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::AwaitingMessages); + 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(), 100000); +} + +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_SUITE_END()