From 7f9a9364b035630895e32c6dd101cbbe583c8b4e Mon Sep 17 00:00:00 2001 From: tequ Date: Mon, 25 May 2026 10:34:24 +0900 Subject: [PATCH] fix: Ensures canonical order for `PriceDataSeries` upon `PriceOracle` creation (#5485) (#744) --- src/test/app/Oracle_test.cpp | 40 +++++++++++++++++++++++++++ src/xrpld/app/tx/detail/SetOracle.cpp | 35 ++++++++++++++++------- 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/src/test/app/Oracle_test.cpp b/src/test/app/Oracle_test.cpp index 44eeb1c9f..42fc81841 100644 --- a/src/test/app/Oracle_test.cpp +++ b/src/test/app/Oracle_test.cpp @@ -568,6 +568,46 @@ private: Oracle oracle(env, {.owner = owner}); oracle.set(UpdateArg{.series = {{"XRP", "USD", 742, 2}}}); } + + // Should be same order as creation + Env env(*this); + auto const baseFee = + static_cast(env.current()->fees().base.drops()); + + auto test = [&](Env& env, DataSeries const& series) { + env.fund(XRP(1'000), owner); + Oracle oracle( + env, {.owner = owner, .series = series, .fee = baseFee}); + BEAST_EXPECT(oracle.exists()); + auto sle = env.le(keylet::oracle(owner, oracle.documentID())); + BEAST_EXPECT( + sle->getFieldArray(sfPriceDataSeries).size() == series.size()); + + auto const beforeQuoteAssetName1 = + sle->getFieldArray(sfPriceDataSeries)[0] + .getFieldCurrency(sfQuoteAsset) + .getText(); + auto const beforeQuoteAssetName2 = + sle->getFieldArray(sfPriceDataSeries)[1] + .getFieldCurrency(sfQuoteAsset) + .getText(); + + oracle.set(UpdateArg{.series = series, .fee = baseFee}); + sle = env.le(keylet::oracle(owner, oracle.documentID())); + + auto const afterQuoteAssetName1 = + sle->getFieldArray(sfPriceDataSeries)[0] + .getFieldCurrency(sfQuoteAsset) + .getText(); + auto const afterQuoteAssetName2 = + sle->getFieldArray(sfPriceDataSeries)[1] + .getFieldCurrency(sfQuoteAsset) + .getText(); + + BEAST_EXPECT(afterQuoteAssetName1 == beforeQuoteAssetName1); + BEAST_EXPECT(afterQuoteAssetName2 == beforeQuoteAssetName2); + }; + test(env, {{"XRP", "USD", 742, 2}, {"XRP", "EUR", 711, 2}}); } void diff --git a/src/xrpld/app/tx/detail/SetOracle.cpp b/src/xrpld/app/tx/detail/SetOracle.cpp index 055143cc6..2f5a72d14 100644 --- a/src/xrpld/app/tx/detail/SetOracle.cpp +++ b/src/xrpld/app/tx/detail/SetOracle.cpp @@ -209,6 +209,17 @@ SetOracle::doApply() { auto const oracleID = keylet::oracle(account_, ctx_.tx[sfOracleDocumentID]); + auto populatePriceData = [](STObject& priceData, STObject const& entry) { + setPriceDataInnerObjTemplate(priceData); + priceData.setFieldCurrency( + sfBaseAsset, entry.getFieldCurrency(sfBaseAsset)); + priceData.setFieldCurrency( + sfQuoteAsset, entry.getFieldCurrency(sfQuoteAsset)); + priceData.setFieldU64(sfAssetPrice, entry.getFieldU64(sfAssetPrice)); + if (entry.isFieldPresent(sfScale)) + priceData.setFieldU8(sfScale, entry.getFieldU8(sfScale)); + }; + if (auto sle = ctx_.view().peek(oracleID)) { // update @@ -249,15 +260,7 @@ SetOracle::doApply() { // add a token pair with the price STObject priceData{sfPriceData}; - setPriceDataInnerObjTemplate(priceData); - priceData.setFieldCurrency( - sfBaseAsset, entry.getFieldCurrency(sfBaseAsset)); - priceData.setFieldCurrency( - sfQuoteAsset, entry.getFieldCurrency(sfQuoteAsset)); - priceData.setFieldU64( - sfAssetPrice, entry.getFieldU64(sfAssetPrice)); - if (entry.isFieldPresent(sfScale)) - priceData.setFieldU8(sfScale, entry.getFieldU8(sfScale)); + populatePriceData(priceData, entry); pairs.emplace(key, std::move(priceData)); } } @@ -285,7 +288,19 @@ SetOracle::doApply() sle->setFieldVL(sfProvider, ctx_.tx[sfProvider]); if (ctx_.tx.isFieldPresent(sfURI)) sle->setFieldVL(sfURI, ctx_.tx[sfURI]); - auto const& series = ctx_.tx.getFieldArray(sfPriceDataSeries); + + STArray series; + std::map, STObject> pairs; + for (auto const& entry : ctx_.tx.getFieldArray(sfPriceDataSeries)) + { + auto const key = tokenPairKey(entry); + STObject priceData{sfPriceData}; + populatePriceData(priceData, entry); + pairs.emplace(key, std::move(priceData)); + } + for (auto const& iter : pairs) + series.push_back(std::move(iter.second)); + sle->setFieldArray(sfPriceDataSeries, series); sle->setFieldVL(sfAssetClass, ctx_.tx[sfAssetClass]); sle->setFieldU32(sfLastUpdateTime, ctx_.tx[sfLastUpdateTime]);