diff --git a/include/xrpl/resource/Fees.h b/include/xrpl/resource/Fees.h index 5001b504d6..a7c923dfef 100644 --- a/include/xrpl/resource/Fees.h +++ b/include/xrpl/resource/Fees.h @@ -13,6 +13,7 @@ extern Charge const kFeeRequestNoReply; // A request that we cannot satisfy. extern Charge const kFeeInvalidSignature; // An object whose signature we had to check that failed. extern Charge const kFeeUselessData; // Data we have no use for. extern Charge const kFeeInvalidData; // Data we have to verify before rejecting. +extern Charge const kFeeMalformedData; // Data that no honest peer would send. // RPC loads extern Charge const kFeeMalformedRpc; // An RPC request that we can immediately tell is invalid. diff --git a/src/libxrpl/resource/Fees.cpp b/src/libxrpl/resource/Fees.cpp index bb825fa3c7..6792dd7e36 100644 --- a/src/libxrpl/resource/Fees.cpp +++ b/src/libxrpl/resource/Fees.cpp @@ -9,6 +9,7 @@ Charge const kFeeRequestNoReply(10, "unsatisfiable request"); Charge const kFeeInvalidSignature(2000, "invalid signature"); Charge const kFeeUselessData(150, "useless data"); Charge const kFeeInvalidData(400, "invalid data"); +Charge const kFeeMalformedData(2000, "malformed data"); Charge const kFeeMalformedRpc(100, "malformed RPC"); Charge const kFeeReferenceRpc(20, "reference RPC"); diff --git a/src/test/app/LedgerReplay_test.cpp b/src/test/app/LedgerReplay_test.cpp index f08d6548e0..27e8a8030b 100644 --- a/src/test/app/LedgerReplay_test.cpp +++ b/src/test/app/LedgerReplay_test.cpp @@ -58,6 +58,8 @@ #include #include #include +#include +#include #include #include #include @@ -958,7 +960,8 @@ struct LedgerReplayer_test : public beast::unit_test::Suite auto reply = std::make_shared( server.msgHandler.processProofPathRequest(request)); BEAST_EXPECT(reply->has_error()); - BEAST_EXPECT(!server.msgHandler.processProofPathResponse(reply)); + BEAST_EXPECT( + server.msgHandler.processProofPathResponse(reply) == ReplayMsgStatus::BadData); } { // request, wrong hash @@ -982,7 +985,7 @@ struct LedgerReplayer_test : public beast::unit_test::Suite auto reply = std::make_shared( server.msgHandler.processProofPathRequest(request)); BEAST_EXPECT(!reply->has_error()); - BEAST_EXPECT(server.msgHandler.processProofPathResponse(reply)); + BEAST_EXPECT(server.msgHandler.processProofPathResponse(reply) == ReplayMsgStatus::Ok); { // bad reply: invalid hash/key sizes @@ -990,37 +993,49 @@ struct LedgerReplayer_test : public beast::unit_test::Suite // reply with undersized ledgerhash (31 bytes) auto bad = std::make_shared(*reply); bad->set_ledgerhash(std::string(31, '\x01')); - BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad)); + BEAST_EXPECT( + server.msgHandler.processProofPathResponse(bad) == + ReplayMsgStatus::Malformed); } { // reply with oversized ledgerhash (33 bytes) auto bad = std::make_shared(*reply); bad->set_ledgerhash(std::string(33, '\x01')); - BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad)); + BEAST_EXPECT( + server.msgHandler.processProofPathResponse(bad) == + ReplayMsgStatus::Malformed); } { // reply with empty ledgerhash auto bad = std::make_shared(*reply); bad->set_ledgerhash(std::string()); - BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad)); + BEAST_EXPECT( + server.msgHandler.processProofPathResponse(bad) == + ReplayMsgStatus::Malformed); } { // reply with undersized key (31 bytes) auto bad = std::make_shared(*reply); bad->set_key(std::string(31, '\x01')); - BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad)); + BEAST_EXPECT( + server.msgHandler.processProofPathResponse(bad) == + ReplayMsgStatus::Malformed); } { // reply with oversized key (33 bytes) auto bad = std::make_shared(*reply); bad->set_key(std::string(33, '\x01')); - BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad)); + BEAST_EXPECT( + server.msgHandler.processProofPathResponse(bad) == + ReplayMsgStatus::Malformed); } { // reply with empty key auto bad = std::make_shared(*reply); bad->set_key(std::string()); - BEAST_EXPECT(!server.msgHandler.processProofPathResponse(bad)); + BEAST_EXPECT( + server.msgHandler.processProofPathResponse(bad) == + ReplayMsgStatus::Malformed); } } @@ -1030,13 +1045,18 @@ struct LedgerReplayer_test : public beast::unit_test::Suite std::string r(reply->ledgerheader()); r.back()--; reply->set_ledgerheader(r); - BEAST_EXPECT(!server.msgHandler.processProofPathResponse(reply)); + BEAST_EXPECT( + server.msgHandler.processProofPathResponse(reply) == + ReplayMsgStatus::Malformed); r.back()++; reply->set_ledgerheader(r); - BEAST_EXPECT(server.msgHandler.processProofPathResponse(reply)); + BEAST_EXPECT( + server.msgHandler.processProofPathResponse(reply) == ReplayMsgStatus::Ok); // bad proof path reply->mutable_path()->RemoveLast(); - BEAST_EXPECT(!server.msgHandler.processProofPathResponse(reply)); + BEAST_EXPECT( + server.msgHandler.processProofPathResponse(reply) == + ReplayMsgStatus::Malformed); } } } @@ -1054,14 +1074,16 @@ struct LedgerReplayer_test : public beast::unit_test::Suite auto reply = std::make_shared( server.msgHandler.processReplayDeltaRequest(request)); BEAST_EXPECT(reply->has_error()); - BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(reply)); + BEAST_EXPECT( + server.msgHandler.processReplayDeltaResponse(reply) == ReplayMsgStatus::BadData); // request, wrong hash uint256 hash(1234567); request->set_ledgerhash(hash.data(), hash.size()); reply = std::make_shared( server.msgHandler.processReplayDeltaRequest(request)); BEAST_EXPECT(reply->has_error()); - BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(reply)); + BEAST_EXPECT( + server.msgHandler.processReplayDeltaResponse(reply) == ReplayMsgStatus::BadData); } { @@ -1071,7 +1093,8 @@ struct LedgerReplayer_test : public beast::unit_test::Suite auto reply = std::make_shared( server.msgHandler.processReplayDeltaRequest(request)); BEAST_EXPECT(!reply->has_error()); - BEAST_EXPECT(server.msgHandler.processReplayDeltaResponse(reply)); + BEAST_EXPECT( + server.msgHandler.processReplayDeltaResponse(reply) == ReplayMsgStatus::Ok); { // bad reply: invalid hash sizes @@ -1079,19 +1102,25 @@ struct LedgerReplayer_test : public beast::unit_test::Suite // reply with undersized ledgerhash (31 bytes) auto bad = std::make_shared(*reply); bad->set_ledgerhash(std::string(31, '\x01')); - BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad)); + BEAST_EXPECT( + server.msgHandler.processReplayDeltaResponse(bad) == + ReplayMsgStatus::Malformed); } { // reply with oversized ledgerhash (33 bytes) auto bad = std::make_shared(*reply); bad->set_ledgerhash(std::string(33, '\x01')); - BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad)); + BEAST_EXPECT( + server.msgHandler.processReplayDeltaResponse(bad) == + ReplayMsgStatus::Malformed); } { // reply with empty ledgerhash auto bad = std::make_shared(*reply); bad->set_ledgerhash(std::string()); - BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(bad)); + BEAST_EXPECT( + server.msgHandler.processReplayDeltaResponse(bad) == + ReplayMsgStatus::Malformed); } } @@ -1101,17 +1130,77 @@ struct LedgerReplayer_test : public beast::unit_test::Suite std::string r(reply->ledgerheader()); r.back()--; reply->set_ledgerheader(r); - BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(reply)); + BEAST_EXPECT( + server.msgHandler.processReplayDeltaResponse(reply) == + ReplayMsgStatus::Malformed); r.back()++; reply->set_ledgerheader(r); - BEAST_EXPECT(server.msgHandler.processReplayDeltaResponse(reply)); + BEAST_EXPECT( + server.msgHandler.processReplayDeltaResponse(reply) == ReplayMsgStatus::Ok); // bad txns reply->mutable_transaction()->RemoveLast(); - BEAST_EXPECT(!server.msgHandler.processReplayDeltaResponse(reply)); + BEAST_EXPECT( + server.msgHandler.processReplayDeltaResponse(reply) == + ReplayMsgStatus::Malformed); } } } + void + testTruncatedHeader() + { + testcase("TruncatedLedgerHeader"); + LedgerServer server(*this, {.initLedgers = 1}); + auto const l = server.ledgerMaster.getClosedLedger(); + + auto runNoThrow = [this](auto fn, char const* what) { + try + { + BEAST_EXPECT(fn() == ReplayMsgStatus::Malformed); + } + catch (std::exception const& e) + { + fail( + std::format("processor threw on truncated header ({}): {}", what, e.what()), + __FILE__, + __LINE__); + } + catch (...) + { + fail( + std::format("processor threw unknown exception ({}) on truncated header", what), + __FILE__, + __LINE__); + } + }; + + { + auto request = std::make_shared(); + request->set_ledgerhash(l->header().hash.data(), l->header().hash.size()); + auto reply = std::make_shared( + server.msgHandler.processReplayDeltaRequest(request)); + BEAST_EXPECT(!reply->has_error()); + + reply->set_ledgerheader(std::string(1, '\x00')); + runNoThrow( + [&] { return server.msgHandler.processReplayDeltaResponse(reply); }, "ReplayDelta"); + } + + { + auto request = std::make_shared(); + request->set_ledgerhash(l->header().hash.data(), l->header().hash.size()); + request->set_type(protocol::TMLedgerMapType::lmACCOUNT_STATE); + request->set_key(keylet::skip().key.data(), keylet::skip().key.size()); + auto reply = std::make_shared( + server.msgHandler.processProofPathRequest(request)); + BEAST_EXPECT(!reply->has_error()); + + reply->set_ledgerheader(std::string(1, '\x00')); + runNoThrow( + [&] { return server.msgHandler.processProofPathResponse(reply); }, "ProofPath"); + } + } + void testTaskParameter() { @@ -1514,6 +1603,7 @@ struct LedgerReplayer_test : public beast::unit_test::Suite { testProofPath(); testReplayDelta(); + testTruncatedHeader(); testTaskParameter(); testConfig(); testHandshake(); diff --git a/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.cpp b/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.cpp index 07738d99f4..6ed4a296ac 100644 --- a/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.cpp +++ b/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.cpp @@ -101,42 +101,54 @@ LedgerReplayMsgHandler::processProofPathRequest( return reply; } -bool +ReplayMsgStatus LedgerReplayMsgHandler::processProofPathResponse( std::shared_ptr const& msg) { protocol::TMProofPathResponse const& reply = *msg; - if (reply.has_error() || !reply.has_key() || !reply.has_ledgerhash() || !reply.has_type() || + if (reply.has_error()) + { + JLOG(journal_.debug()) << "ProofPathResponse: peer reported error"; + return ReplayMsgStatus::BadData; + } + if (!reply.has_key() || !reply.has_ledgerhash() || !reply.has_type() || !reply.has_ledgerheader() || reply.path_size() == 0 || reply.ledgerhash().size() != uint256::size() || reply.key().size() != uint256::size()) { - JLOG(journal_.debug()) << "Bad message: Error reply"; - return false; + JLOG(journal_.debug()) << "ProofPathResponse: malformed (missing or wrong-size fields)"; + return ReplayMsgStatus::Malformed; } if (reply.type() != protocol::lmACCOUNT_STATE) { - JLOG(journal_.debug()) << "Bad message: we only support the state ShaMap for now"; - return false; + JLOG(journal_.debug()) << "ProofPathResponse: malformed (unsupported map type)"; + return ReplayMsgStatus::Malformed; } // deserialize the header - auto info = deserializeHeader({reply.ledgerheader().data(), reply.ledgerheader().size()}); + LedgerHeader info; + try + { + info = deserializeHeader(makeSlice(reply.ledgerheader())); + } + catch (std::exception const& e) + { + JLOG(journal_.debug()) << "ProofPathResponse: malformed header (" << e.what() << ")"; + return ReplayMsgStatus::Malformed; + } uint256 const replyHash = uint256::fromRaw(reply.ledgerhash()); if (calculateLedgerHash(info) != replyHash) { - JLOG(journal_.debug()) << "Bad message: Hash mismatch"; - return false; + JLOG(journal_.debug()) << "ProofPathResponse: malformed (hash mismatch)"; + return ReplayMsgStatus::Malformed; } info.hash = replyHash; uint256 const key = uint256::fromRaw(reply.key()); if (key != keylet::skip().key) { - JLOG(journal_.debug()) << "Bad message: we only support the short skip list for now. " - "Key in reply " - << key; - return false; + JLOG(journal_.debug()) << "ProofPathResponse: malformed (unexpected key " << key << ")"; + return ReplayMsgStatus::Malformed; } // verify the skip list @@ -149,26 +161,35 @@ LedgerReplayMsgHandler::processProofPathResponse( if (!SHAMap::verifyProofPath(info.accountHash, key, path)) { - JLOG(journal_.debug()) << "Bad message: Proof path verify failed"; - return false; + JLOG(journal_.debug()) << "ProofPathResponse: malformed (proof path verify failed)"; + return ReplayMsgStatus::Malformed; } // deserialize the SHAMapItem - auto node = SHAMapTreeNode::makeFromWire(makeSlice(path.front())); + SHAMapTreeNodePtr node; + try + { + node = SHAMapTreeNode::makeFromWire(makeSlice(path.front())); + } + catch (std::exception const& e) + { + JLOG(journal_.debug()) << "ProofPathResponse: malformed SHAMap node (" << e.what() << ")"; + return ReplayMsgStatus::Malformed; + } if (!node || !node->isLeaf()) { - JLOG(journal_.debug()) << "Bad message: Cannot deserialize"; - return false; + JLOG(journal_.debug()) << "ProofPathResponse: malformed (not a leaf node)"; + return ReplayMsgStatus::Malformed; } if (auto item = safeDowncast(node.get())->peekItem()) { replayer_.gotSkipList(info, item); - return true; + return ReplayMsgStatus::Ok; } - JLOG(journal_.debug()) << "Bad message: Cannot get ShaMapItem"; - return false; + JLOG(journal_.debug()) << "ProofPathResponse: malformed (no SHAMapItem)"; + return ReplayMsgStatus::Malformed; } protocol::TMReplayDeltaResponse @@ -210,24 +231,38 @@ LedgerReplayMsgHandler::processReplayDeltaRequest( return reply; } -bool +ReplayMsgStatus LedgerReplayMsgHandler::processReplayDeltaResponse( std::shared_ptr const& msg) { protocol::TMReplayDeltaResponse const& reply = *msg; - if (reply.has_error() || !reply.has_ledgerheader() || !reply.has_ledgerhash() || + if (reply.has_error()) + { + JLOG(journal_.debug()) << "ReplayDeltaResponse: peer reported error"; + return ReplayMsgStatus::BadData; + } + if (!reply.has_ledgerheader() || !reply.has_ledgerhash() || reply.ledgerhash().size() != uint256::size()) { - JLOG(journal_.debug()) << "Bad message: Error reply"; - return false; + JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed (missing or wrong-size fields)"; + return ReplayMsgStatus::Malformed; } - auto info = deserializeHeader({reply.ledgerheader().data(), reply.ledgerheader().size()}); + LedgerHeader info; + try + { + info = deserializeHeader(makeSlice(reply.ledgerheader())); + } + catch (std::exception const& e) + { + JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed header (" << e.what() << ")"; + return ReplayMsgStatus::Malformed; + } uint256 const replyHash = uint256::fromRaw(reply.ledgerhash()); if (calculateLedgerHash(info) != replyHash) { - JLOG(journal_.debug()) << "Bad message: Hash mismatch"; - return false; + JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed (hash mismatch)"; + return ReplayMsgStatus::Malformed; } info.hash = replyHash; @@ -252,8 +287,8 @@ LedgerReplayMsgHandler::processReplayDeltaResponse( auto tx = std::make_shared(txSit); if (!tx) { - JLOG(journal_.debug()) << "Bad message: Cannot deserialize"; - return false; + JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed (tx deserialize)"; + return ReplayMsgStatus::Malformed; } auto tid = tx->getTransactionID(); STObject meta(metaSit, sfMetadata); @@ -262,25 +297,26 @@ LedgerReplayMsgHandler::processReplayDeltaResponse( if (!txMap.addGiveItem( SHAMapNodeType::TnTransactionMd, makeShamapitem(tid, shaMapItemData.slice()))) { - JLOG(journal_.debug()) << "Bad message: Cannot deserialize"; - return false; + JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed (tx map add)"; + return ReplayMsgStatus::Malformed; } } } - catch (std::exception const&) + catch (std::exception const& e) { - JLOG(journal_.debug()) << "Bad message: Cannot deserialize"; - return false; + JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed transactions (" << e.what() + << ")"; + return ReplayMsgStatus::Malformed; } if (txMap.getHash().asUInt256() != info.txHash) { - JLOG(journal_.debug()) << "Bad message: Transactions verify failed"; - return false; + JLOG(journal_.debug()) << "ReplayDeltaResponse: malformed (transactions verify failed)"; + return ReplayMsgStatus::Malformed; } replayer_.gotReplayDelta(info, std::move(orderedTxns)); - return true; + return ReplayMsgStatus::Ok; } } // namespace xrpl diff --git a/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h b/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h index 5a8951fb25..ba989e2586 100644 --- a/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h +++ b/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h @@ -10,6 +10,15 @@ namespace xrpl { class Application; class LedgerReplayer; +/** + * Outcome of processing an incoming ledger-replay response. + */ +enum class ReplayMsgStatus { + Ok, ///< Accepted. + BadData, ///< Peer reported has_error() (legitimate "cannot fulfill" signal). + Malformed, ///< Protocol-level violation; no honest peer would produce this. +}; + class LedgerReplayMsgHandler final { public: @@ -19,31 +28,31 @@ public: /** * Process TMProofPathRequest and return TMProofPathResponse * @note check has_error() and error() of the response for error + * @return TMProofPathResponse with the proof path, or with error() set if + * the request cannot be fulfilled */ protocol::TMProofPathResponse processProofPathRequest(std::shared_ptr const& msg); /** * Process TMProofPathResponse - * @return false if the response message has bad format or bad data; - * true otherwise */ - bool + ReplayMsgStatus processProofPathResponse(std::shared_ptr const& msg); /** * Process TMReplayDeltaRequest and return TMReplayDeltaResponse * @note check has_error() and error() of the response for error + * @return TMReplayDeltaResponse with the ledger delta, or with error() set + * if the request cannot be fulfilled */ protocol::TMReplayDeltaResponse processReplayDeltaRequest(std::shared_ptr const& msg); /** * Process TMReplayDeltaResponse - * @return false if the response message has bad format or bad data; - * true otherwise */ - bool + ReplayMsgStatus processReplayDeltaResponse(std::shared_ptr const& msg); private: diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index 8838970b5f..35eecc6439 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -1565,9 +1565,16 @@ PeerImp::onMessage(std::shared_ptr const& m) return; } - if (!ledgerReplayMsgHandler_.processProofPathResponse(m)) + switch (ledgerReplayMsgHandler_.processProofPathResponse(m)) { - fee_.update(Resource::kFeeInvalidData, "proof_path_response"); + case ReplayMsgStatus::Ok: + break; + case ReplayMsgStatus::BadData: + fee_.update(Resource::kFeeInvalidData, "proof_path_response"); + break; + case ReplayMsgStatus::Malformed: + fee_.update(Resource::kFeeMalformedData, "proof_path_response malformed"); + break; } } @@ -1615,9 +1622,16 @@ PeerImp::onMessage(std::shared_ptr const& m) return; } - if (!ledgerReplayMsgHandler_.processReplayDeltaResponse(m)) + switch (ledgerReplayMsgHandler_.processReplayDeltaResponse(m)) { - fee_.update(Resource::kFeeInvalidData, "replay_delta_response"); + case ReplayMsgStatus::Ok: + break; + case ReplayMsgStatus::BadData: + fee_.update(Resource::kFeeInvalidData, "replay_delta_response"); + break; + case ReplayMsgStatus::Malformed: + fee_.update(Resource::kFeeMalformedData, "replay_delta_response malformed"); + break; } }