diff --git a/API-CHANGELOG.md b/API-CHANGELOG.md index a04f2653285..90df922e207 100644 --- a/API-CHANGELOG.md +++ b/API-CHANGELOG.md @@ -53,6 +53,7 @@ This section contains changes targeting a future version. - `submit`: The `fail_hard` field now returns an error if the value is not a boolean. [#6529](https://github.com/XRPLF/rippled/pull/6529) - `subscribe`: The `taker` field in the `books` array now returns `actMalformed` instead of `badIssuer` if the value is not a valid account. [#6529](https://github.com/XRPLF/rippled/pull/6529) - Fixed a bug in `Forwarded` HTTP header parsing where the extracted IP address could be incorrect when no comma or semicolon delimiter follows the address. This could cause the server to misidentify a client's IP address when operating behind a reverse proxy. [#6529](https://github.com/XRPLF/rippled/pull/6529) +- `book_offers`: The `domain` field now returns `objectNotFound` if the specified permissioned domain does not exist in the ledger, instead of silently returning an empty `offers` array. [#7963](https://github.com/XRPLF/rippled/issues/7963) ## XRP Ledger server version 3.1.0 diff --git a/src/test/rpc/Book_test.cpp b/src/test/rpc/Book_test.cpp index 83f7b64b4b1..821172104c8 100644 --- a/src/test/rpc/Book_test.cpp +++ b/src/test/rpc/Book_test.cpp @@ -1532,6 +1532,38 @@ class Book_test : public beast::unit_test::Suite BEAST_EXPECT(jrr[jss::error] == "domainMalformed"); BEAST_EXPECT(jrr[jss::error_message] == "Unable to parse domain."); } + + // Well-formed domain that does not exist in the ledger. + { + json::Value jvParams; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::taker_pays][jss::currency] = "USD"; + jvParams[jss::taker_pays][jss::issuer] = gw.human(); + jvParams[jss::taker_gets][jss::currency] = "EUR"; + jvParams[jss::taker_gets][jss::issuer] = gw.human(); + jvParams[jss::domain] = std::string(64, 'A'); + auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::error] == "objectNotFound"); + BEAST_EXPECT(jrr[jss::error_message] == "Domain not found."); + } + + // An all-zero domain is well-formed hex but can never name an object. + // Both ledger views are covered: a validated ledger reaches + // Ledger::read, which treats a zero key as UNREACHABLE, so dropping + // the isZero() guard in doBookOffers aborts on this case. + for (auto const* ledgerIndex : {"validated", "current"}) + { + json::Value jvParams; + jvParams[jss::ledger_index] = ledgerIndex; + jvParams[jss::taker_pays][jss::currency] = "USD"; + jvParams[jss::taker_pays][jss::issuer] = gw.human(); + jvParams[jss::taker_gets][jss::currency] = "EUR"; + jvParams[jss::taker_gets][jss::issuer] = gw.human(); + jvParams[jss::domain] = std::string(64, '0'); + auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::error] == "objectNotFound"); + BEAST_EXPECT(jrr[jss::error_message] == "Domain not found."); + } } void @@ -1662,6 +1694,22 @@ class Book_test : public beast::unit_test::Suite checkBookOffers(jrr); } + // book_offers: an unknown domain is rejected rather than silently + // returning an empty book + { + json::Value jvParams; + jvParams[jss::taker] = env.master.human(); + jvParams[jss::taker_pays][jss::currency] = "XRP"; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::taker_gets][jss::currency] = "USD"; + jvParams[jss::taker_gets][jss::issuer] = gw.human(); + jvParams[jss::domain] = std::string(64, 'A'); + + auto const jrr = env.rpc("json", "book_offers", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::error] == "objectNotFound"); + BEAST_EXPECT(jrr[jss::error_message] == "Domain not found."); + } + // subscribe to domain book should return domain offer { json::Value books; diff --git a/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp b/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp index 63dee76f1ba..a0fac97e9e3 100644 --- a/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp +++ b/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -232,6 +233,16 @@ doBookOffers(RPC::JsonContext& context) return RPC::makeError(RpcDomainMalformed, "Unable to parse domain."); } + // A PermissionedDomain's DomainID is its ledger key, so the domain + // exists only if that entry is present. `read` is used rather than + // `exists` because the caller supplies the raw key: `exists` does not + // check the entry type, so the index of any unrelated object would + // pass. The all-zero key is well-formed hex but can never name a real + // object, and `Ledger::read` treats it as UNREACHABLE, so it must be + // rejected before reaching the view. + if (num.isZero() || !lpLedger->read(keylet::permissionedDomain(num))) + return RPC::makeError(RpcObjectNotFound, "Domain not found."); + domain = num; }