fix: Deduplicate oracle entries in get_aggregate_price RPC (#6586)

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Mayukha Vadari <mvadari@ripple.com>
Co-authored-by: Bart <bthomee@users.noreply.github.com>
This commit is contained in:
Matt Van Horn
2026-08-07 13:24:45 -07:00
committed by GitHub
parent abf5511d07
commit 798e889ec4
3 changed files with 51 additions and 0 deletions

View File

@@ -42,6 +42,7 @@ This section contains changes targeting a future version.
### Bugfixes
- `get_aggregate_price`: Duplicate entries in the `oracles` request array are now ignored. [#6586](https://github.com/XRPLF/rippled/pull/6586)
- Peer Crawler: The `port` field in `overlay.active[]` now consistently returns an integer instead of a string for outbound peers. [#6318](https://github.com/XRPLF/rippled/pull/6318)
- `ping`: The `ip` field is no longer returned as an empty string for proxied connections without a forwarded-for header. It is now omitted, consistent with the behavior for identified connections. [#6730](https://github.com/XRPLF/rippled/pull/6730)
- gRPC `GetLedgerDiff`: Fixed error message that incorrectly said "base ledger not validated" when the desired ledger was not validated. [#6730](https://github.com/XRPLF/rippled/pull/6730)

View File

@@ -320,6 +320,48 @@ public:
BEAST_EXPECT(ret[jss::median] == "74");
BEAST_EXPECT(ret[jss::time] == 946695000);
}
// Duplicate oracle entries should be deduplicated.
// Two separate oracles with different prices give size=2.
// Listing the first oracle twice in the query must not
// inflate the size to 3.
{
Env env(*this);
auto const baseFee = static_cast<int>(env.current()->fees().base.drops());
Account const owner1{"owner1"};
Account const owner2{"owner2"};
env.fund(XRP(1'000), owner1);
env.fund(XRP(1'000), owner2);
Oracle const oracle1(
env, {.owner = owner1, .series = {{"XRP", "USD", 740, 1}}, .fee = baseFee});
Oracle const oracle2(
env, {.owner = owner2, .series = {{"XRP", "USD", 840, 1}}, .fee = baseFee});
// Query with both oracles listed once
OraclesData const single = {
{owner1, oracle1.documentID()}, {owner2, oracle2.documentID()}};
auto const retSingle = Oracle::aggregatePrice(env, "XRP", "USD", single);
// Query with oracle1 listed twice
OraclesData const duplicated = {
{owner1, oracle1.documentID()},
{owner1, oracle1.documentID()},
{owner2, oracle2.documentID()}};
auto const retDup = Oracle::aggregatePrice(env, "XRP", "USD", duplicated);
// Results should be identical - duplicates must not be
// double-counted
BEAST_EXPECT(
retSingle[jss::entire_set][jss::size] == retDup[jss::entire_set][jss::size]);
BEAST_EXPECT(retDup[jss::entire_set][jss::size].asUInt() == 2);
BEAST_EXPECT(
retSingle[jss::entire_set][jss::mean] == retDup[jss::entire_set][jss::mean]);
BEAST_EXPECT(
retSingle[jss::entire_set][jss::standard_deviation] ==
retDup[jss::entire_set][jss::standard_deviation]);
BEAST_EXPECT(retSingle[jss::median] == retDup[jss::median]);
}
}
void

View File

@@ -33,7 +33,9 @@
#include <memory>
#include <numeric>
#include <optional>
#include <set>
#include <tuple>
#include <utility>
#include <variant>
namespace xrpl {
@@ -251,6 +253,8 @@ doGetAggregatePrice(rpc::JsonContext& context)
// Collect the dataset into bimap keyed by lastUpdateTime and
// STAmount (Number is int64 and price is uint64)
Prices prices;
// Track seen {account, documentID} pairs to skip duplicates
std::set<std::pair<AccountID, std::uint32_t>> seen;
for (auto const& oracle : params[jss::oracles])
{
if (!oracle.isMember(jss::oracle_document_id) || !oracle.isMember(jss::account))
@@ -268,6 +272,10 @@ doGetAggregatePrice(rpc::JsonContext& context)
return result;
}
// Skip duplicate oracle entries
if (!seen.emplace(*account, *documentID).second)
continue;
auto const sle = ledger->read(keylet::oracle(*account, *documentID));
iteratePriceData(context, sle, [&](STObject const& node) {
auto const& series = node.getFieldArray(sfPriceDataSeries);