diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -22,3 +22,10 @@ - The `importmulti` RPC will now contain a new per-request `warnings` field with strings that explain when fields are being ignored or inconsistent, if any. + +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 IBD is + completed. 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_RESULTS = 2000; // After the 1000th addr, the seeder will only add one more address per addr // message. static const unsigned int ADDR_SOFT_CAP = 1000; @@ -28,6 +29,13 @@ Finished, }; +enum class CheckpointState { + NotChecked, + HasLatest, + NotFound, + IncompleteChain, +}; + class CSeederNode { private: SOCKET sock; @@ -42,6 +50,7 @@ int ban; int64_t doneAfter; CAddress you; + CheckpointState checkpointState; int GetTimeout() { return you.IsTor() ? 120 : 30; } diff --git a/src/seeder/bitcoin.cpp b/src/seeder/bitcoin.cpp --- a/src/seeder/bitcoin.cpp +++ b/src/seeder/bitcoin.cpp @@ -117,12 +117,50 @@ vRecv.SetVersion(std::min(nVersion, PROTOCOL_VERSION)); // tfm::format(std::cout, "\n%s: version %i\n", ToString(you), // nVersion); + // Create a getheaders message so the seeder can check the node's chain + // state + BeginMessage(NetMsgType::GETHEADERS); + // Locator hash: Seeder requests the headers after the latest checkpoint + const CChainParams &chainParams = Params(); + const auto checkpoints = chainParams.Checkpoints().mapCheckpoints; + std::vector locatorHashes(1, checkpoints.rbegin()->second); + vSend << PROTOCOL_VERSION << locatorHashes << uint256(); + EndMessage(); + } + + if (strCommand == NetMsgType::HEADERS) { + std::vector headers; + recv >> headers; + const CChainParams &chainParams = Params(); + doneAfter = 0; + checkpointState = CheckpointState::NotFound; + // If the node has this checkpoint then it will return the first + // MAX_HEADERS_RESULTS 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 (headers.size() > 0 && headers.size() <= MAX_HEADERS_RESULTS && + chainParams.Checkpoints().mapCheckpoints.rbegin()->second == + headers[0].hashPrevBlock) { + doneAfter = 1; + checkpointState = CheckpointState::HasLatest; + } else if (headers.size() == 0) { + // This is an ambiguous situation where the node either only has the + // genesis block or has all the blocks up to the checkpoint and + // nothing further. While not technically in IBD, the node is + // missing blocks and should be marked bad, but not banned. + checkpointState = CheckpointState::IncompleteChain; + } + // Seeder is finished with a node after it has received ADDR_SOFT_CAP + // addrs from a node AND after the seeder has checked the node's chain + if (!vAddr || vAddr->size() > ADDR_SOFT_CAP) { + doneAfter = checkpointState == CheckpointState::HasLatest ? 1 : 0; + ban = checkpointState == CheckpointState::NotFound ? 100000 : 0; + return PeerMessagingState::Finished; + } if (vAddr) { - BeginMessage("getaddr"); + BeginMessage(NetMsgType::GETADDR); EndMessage(); - doneAfter = time(nullptr) + GetTimeout(); - } else { - doneAfter = time(nullptr) + 1; } return PeerMessagingState::AwaitingMessages; } @@ -156,13 +194,24 @@ // ToString(you), // addr.ToString(), (int)(vAddr->size())); if (vAddr->size() > ADDR_SOFT_CAP) { - doneAfter = 1; - return PeerMessagingState::Finished; + // Seeder is finished with a node after it has received more + // than ADDR_SOFT_CAP addrs from a node AND after the seeder has + // checked the node's chain + if (checkpointState != CheckpointState::NotChecked) { + doneAfter = + checkpointState == CheckpointState::HasLatest ? 1 : 0; + ban = checkpointState == CheckpointState::NotFound ? 100000 + : 0; + return PeerMessagingState::Finished; + } + break; } } return PeerMessagingState::AwaitingMessages; } + // Update socket timeout + doneAfter = time(nullptr) + GetTimeout(); return PeerMessagingState::AwaitingMessages; } @@ -228,7 +277,8 @@ CSeederNode::CSeederNode(const CService &ip, std::vector *vAddrIn) : sock(INVALID_SOCKET), vSend(SER_NETWORK, 0), vRecv(SER_NETWORK, 0), nHeaderStart(-1), nMessageStart(-1), nVersion(0), vAddr(vAddrIn), ban(0), - doneAfter(0), you(ip, ServiceFlags(NODE_NETWORK | NODE_BITCOIN_CASH)) { + doneAfter(0), you(ip, ServiceFlags(NODE_NETWORK | NODE_BITCOIN_CASH)), + checkpointState(CheckpointState::NotChecked) { if (time(nullptr) > 1329696000) { vSend.SetVersion(209); vRecv.SetVersion(209); @@ -325,5 +375,5 @@ } close(sock); sock = INVALID_SOCKET; - return (ban == 0) && res; + return (ban == 0) && res && (checkpointState == CheckpointState::HasLatest); } 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 @@ -30,7 +31,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, @@ -117,7 +118,7 @@ sendAddrs.resize(1); addrMessage = CreateAddrMessage(sendAddrs); testNode->TestProcessMessage(NetMsgType::ADDR, addrMessage, - PeerMessagingState::Finished); + PeerMessagingState::AwaitingMessages); BOOST_CHECK_EQUAL(ADDR_SOFT_CAP + 1, vAddr.size()); // Test the seeder's behavior after ADDR_SOFT_CAP addrs @@ -127,10 +128,222 @@ 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 HeaderMessageContent { + WRONG_CHAIN, + GOOD_HEADER, + NO_HEADERS, + TOO_MANY_HEADERS, +}; + +static CDataStream +CreateHeadersMessage(HeaderMessageContent 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 == HeaderMessageContent::NO_HEADERS) { + payload << 0; + return payload; + } + uint16_t numberOfHeaders = 1; + if (headersMessageContent == HeaderMessageContent::TOO_MANY_HEADERS) { + numberOfHeaders = MAX_HEADERS_RESULTS + 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 == HeaderMessageContent::WRONG_CHAIN) { + previousBlockHash = BlockHash(garbageHash); + } + + 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_fill_addr_after_finding_checkpoint_test) { + // Happy path + CDataStream headersMessage = + CreateHeadersMessage(HeaderMessageContent::GOOD_HEADER); + testNode->TestProcessMessage(NetMsgType::HEADERS, headersMessage, + PeerMessagingState::AwaitingMessages); + + BOOST_CHECK_EQUAL(testNode->GetBan(), 0); + std::vector sendAddrs(ADDR_SOFT_CAP, vAddr[0]); + CDataStream addrMessage = CreateAddrMessage(sendAddrs); + BOOST_CHECK_EQUAL(1, vAddr.size()); + testNode->TestProcessMessage(NetMsgType::ADDR, addrMessage, + PeerMessagingState::Finished); + BOOST_CHECK_EQUAL(ADDR_SOFT_CAP + 1, vAddr.size()); + BOOST_CHECK_EQUAL(testNode->GetBan(), 0); +} + +BOOST_AUTO_TEST_CASE(seeder_node_headers_wrong_chain_test) { + // Seeder receives a HEADERS message obviously following the wrong chain + // This type of node should be banned after filling vAddr. + CDataStream headersMessage = + CreateHeadersMessage(HeaderMessageContent::WRONG_CHAIN); + testNode->TestProcessMessage(NetMsgType::HEADERS, headersMessage, + PeerMessagingState::AwaitingMessages); + + // Banning should not occur until after addrs are finished being received. + BOOST_CHECK_EQUAL(testNode->GetBan(), 0); + std::vector sendAddrs(ADDR_SOFT_CAP, vAddr[0]); + CDataStream addrMessage = CreateAddrMessage(sendAddrs); + BOOST_CHECK_EQUAL(1, vAddr.size()); + testNode->TestProcessMessage(NetMsgType::ADDR, addrMessage, + PeerMessagingState::Finished); + BOOST_CHECK_EQUAL(ADDR_SOFT_CAP + 1, vAddr.size()); + 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 + // This is a misbehaving node and should be banned after filling vAddr. + CDataStream headersMessage = + CreateHeadersMessage(HeaderMessageContent::TOO_MANY_HEADERS); + testNode->TestProcessMessage(NetMsgType::HEADERS, headersMessage, + PeerMessagingState::AwaitingMessages); + + // Banning should not occur until after addrs are finished being received. + BOOST_CHECK_EQUAL(testNode->GetBan(), 0); + std::vector sendAddrs(ADDR_SOFT_CAP, vAddr[0]); + CDataStream addrMessage = CreateAddrMessage(sendAddrs); + BOOST_CHECK_EQUAL(1, vAddr.size()); + testNode->TestProcessMessage(NetMsgType::ADDR, addrMessage, + PeerMessagingState::Finished); + BOOST_CHECK_EQUAL(ADDR_SOFT_CAP + 1, vAddr.size()); + BOOST_CHECK_EQUAL(testNode->GetBan(), 100000); +} + +BOOST_AUTO_TEST_CASE(seeder_node_empty_headers_test) { + // Seeder receives an empty HEADERS message before filling vAddr + // 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 do not ban. + CDataStream headersMessage = + CreateHeadersMessage(HeaderMessageContent::NO_HEADERS); + testNode->TestProcessMessage(NetMsgType::HEADERS, headersMessage, + PeerMessagingState::AwaitingMessages); + + BOOST_CHECK_EQUAL(testNode->GetBan(), 0); + std::vector sendAddrs(ADDR_SOFT_CAP, vAddr[0]); + CDataStream addrMessage = CreateAddrMessage(sendAddrs); + BOOST_CHECK_EQUAL(1, vAddr.size()); + testNode->TestProcessMessage(NetMsgType::ADDR, addrMessage, + PeerMessagingState::Finished); + BOOST_CHECK_EQUAL(ADDR_SOFT_CAP + 1, vAddr.size()); + BOOST_CHECK_EQUAL(testNode->GetBan(), 0); +} + +BOOST_AUTO_TEST_CASE(seeder_node_found_checkpoint_after_addrs_test) { + // Happy path except the headers come after the addrs + std::vector sendAddrs(ADDR_SOFT_CAP, vAddr[0]); + CDataStream addrMessage = CreateAddrMessage(sendAddrs); + BOOST_CHECK_EQUAL(1, vAddr.size()); + testNode->TestProcessMessage(NetMsgType::ADDR, addrMessage, + PeerMessagingState::AwaitingMessages); + BOOST_CHECK_EQUAL(ADDR_SOFT_CAP + 1, vAddr.size()); + BOOST_CHECK_EQUAL(testNode->GetBan(), 0); + + CDataStream headersMessage = + CreateHeadersMessage(HeaderMessageContent::GOOD_HEADER); + testNode->TestProcessMessage(NetMsgType::HEADERS, headersMessage, + PeerMessagingState::Finished); + BOOST_CHECK_EQUAL(testNode->GetBan(), 0); +} + +BOOST_AUTO_TEST_CASE( + seeder_node_addr_before_and_after_finding_checkpoint_test) { + // Addrs come before and after the headers message. The first addr does not + // fill vAddr, so the seeder should wait for more addrs before it is + // finished with the node + std::vector sendAddrs(ADDR_SOFT_CAP - 1, vAddr[0]); + CDataStream addrMessage = CreateAddrMessage(sendAddrs); + BOOST_CHECK_EQUAL(1, vAddr.size()); + testNode->TestProcessMessage(NetMsgType::ADDR, addrMessage, + PeerMessagingState::AwaitingMessages); + BOOST_CHECK_EQUAL(ADDR_SOFT_CAP, vAddr.size()); + + CDataStream headersMessage = + CreateHeadersMessage(HeaderMessageContent::GOOD_HEADER); + testNode->TestProcessMessage(NetMsgType::HEADERS, headersMessage, + PeerMessagingState::AwaitingMessages); + BOOST_CHECK_EQUAL(testNode->GetBan(), 0); + + sendAddrs.resize(1); + addrMessage = CreateAddrMessage(sendAddrs); + BOOST_CHECK_EQUAL(ADDR_SOFT_CAP, vAddr.size()); + testNode->TestProcessMessage(NetMsgType::ADDR, addrMessage, + PeerMessagingState::Finished); + BOOST_CHECK_EQUAL(ADDR_SOFT_CAP + 1, vAddr.size()); + BOOST_CHECK_EQUAL(testNode->GetBan(), 0); +} + +BOOST_AUTO_TEST_CASE(seeder_node_wrong_chain_after_addrs_test) { + // addrs come before the headers from the wrong chain + std::vector sendAddrs(ADDR_SOFT_CAP, vAddr[0]); + CDataStream addrMessage = CreateAddrMessage(sendAddrs); + BOOST_CHECK_EQUAL(1, vAddr.size()); + testNode->TestProcessMessage(NetMsgType::ADDR, addrMessage, + PeerMessagingState::AwaitingMessages); + BOOST_CHECK_EQUAL(ADDR_SOFT_CAP + 1, vAddr.size()); + + // Banning should not occur until after addrs are finished being received. + BOOST_CHECK_EQUAL(testNode->GetBan(), 0); + CDataStream headersMessage = + CreateHeadersMessage(HeaderMessageContent::WRONG_CHAIN); + testNode->TestProcessMessage(NetMsgType::HEADERS, headersMessage, + PeerMessagingState::Finished); + BOOST_CHECK_EQUAL(testNode->GetBan(), 100000); +} + +BOOST_AUTO_TEST_CASE( + seeder_node_addr_before_and_after_finding_wrong_chain_test) { + // Addrs come before and after the headers message. The first addr does not + // fill vAddr, so the seeder should wait for more addrs before it is + // finished with the node. The node is on the wrong chain + std::vector sendAddrs(ADDR_SOFT_CAP - 1, vAddr[0]); + CDataStream addrMessage = CreateAddrMessage(sendAddrs); + BOOST_CHECK_EQUAL(1, vAddr.size()); + testNode->TestProcessMessage(NetMsgType::ADDR, addrMessage, + PeerMessagingState::AwaitingMessages); + BOOST_CHECK_EQUAL(ADDR_SOFT_CAP, vAddr.size()); + + // Banning should not occur until after addrs are finished being received. + BOOST_CHECK_EQUAL(testNode->GetBan(), 0); + CDataStream headersMessage = + CreateHeadersMessage(HeaderMessageContent::WRONG_CHAIN); + testNode->TestProcessMessage(NetMsgType::HEADERS, headersMessage, + PeerMessagingState::AwaitingMessages); + BOOST_CHECK_EQUAL(testNode->GetBan(), 0); + + sendAddrs.resize(1); + addrMessage = CreateAddrMessage(sendAddrs); + BOOST_CHECK_EQUAL(ADDR_SOFT_CAP, vAddr.size()); + testNode->TestProcessMessage(NetMsgType::ADDR, addrMessage, + PeerMessagingState::Finished); + BOOST_CHECK_EQUAL(ADDR_SOFT_CAP + 1, vAddr.size()); + BOOST_CHECK_EQUAL(testNode->GetBan(), 100000); +} + BOOST_AUTO_TEST_SUITE_END()