diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -15,3 +15,11 @@ RPC changes ----------- The `getblockstats` RPC is faster for fee calculation by using BlockUndo data. Also, `-txindex` is no longer required and `getblockstats` works for all non-pruned blocks. + +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 @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -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 @@ -104,13 +104,64 @@ // 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 + // 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; + if (checkpointState == CheckpointState::NotFound) { + ban = 100000; + } + return PeerMessagingState::Finished; + } + break; } } return PeerMessagingState::AwaitingMessages; } + // A IBD node will not send this message and therefore will timeout with + // checkpointState = CheckpointState::NotChecked + if (strCommand == NetMsgType::HEADERS && vAddr) { + 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() > MAX_HEADERS_RESULTS) { + tfm::format(std::cout, + "%s: Misbehaving: oversized HEADER message\n", + ToString(you)); + } 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; + } else if (chainParams.Checkpoints().mapCheckpoints.rbegin()->second == + headers[0].hashPrevBlock) { + doneAfter = 1; + checkpointState = CheckpointState::HasLatest; + } + // 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->size() > ADDR_SOFT_CAP) { + if (checkpointState == CheckpointState::NotFound) { + ban = 100000; + } + return PeerMessagingState::Finished; + } + // Update socket timeout + doneAfter = time(nullptr) + GetTimeout(); + return PeerMessagingState::AwaitingMessages; + } + return PeerMessagingState::AwaitingMessages; } @@ -176,7 +227,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); @@ -282,5 +334,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,225 @@ 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_before_finding_checkpoint_test) { + // Happy path + 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_headers_wrong_chain_test) { + // Seeder receives a HEADERS message obviously following the wrong chain + // This type of node should be banned after filling vAddr. + 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_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. + 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::TOO_MANY_HEADERS); + 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 after 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. + */ + 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::NO_HEADERS); + testNode->TestProcessMessage(NetMsgType::HEADERS, headersMessage, + PeerMessagingState::Finished); + BOOST_CHECK_EQUAL(testNode->GetBan(), 0); +} + +BOOST_AUTO_TEST_CASE(seeder_node_found_checkpoint_before_addrs_test) { + // Happy path except the addrs come after the headers + 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_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_before_addrs_test) { + // Happy path except the addrs come after the headers from the wrong chain + 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_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()