rippled
GetAggregatePrice.cpp
1 //------------------------------------------------------------------------------
2 /*
3  This file is part of rippled: https://github.com/ripple/rippled
4  Copyright (c) 2023 Ripple Labs Inc.
5 
6  Permission to use, copy, modify, and/or distribute this software for any
7  purpose with or without fee is hereby granted, provided that the above
8  copyright notice and this permission notice appear in all copies.
9 
10  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11  WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12  MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13  ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 */
18 //==============================================================================
19 
20 #include <ripple/app/ledger/LedgerMaster.h>
21 #include <ripple/app/main/Application.h>
22 #include <ripple/json/json_value.h>
23 #include <ripple/ledger/ReadView.h>
24 #include <ripple/protocol/ErrorCodes.h>
25 #include <ripple/protocol/jss.h>
26 #include <ripple/rpc/Context.h>
27 #include <ripple/rpc/impl/RPCHelpers.h>
28 
29 #include <boost/bimap.hpp>
30 #include <boost/bimap/multiset_of.hpp>
31 
32 namespace ripple {
33 
34 using namespace boost::bimaps;
35 // sorted descending by lastUpdateTime, ascending by AssetPrice
36 using Prices = bimap<
37  multiset_of<std::uint32_t, std::greater<std::uint32_t>>,
38  multiset_of<STAmount>>;
39 
43 static void
45  RPC::JsonContext& context,
46  std::shared_ptr<SLE const> const& sle,
47  std::function<bool(STObject const&)>&& f)
48 {
50  constexpr std::uint8_t maxHistory = 3;
51  bool isNew = false;
52  std::uint8_t history = 0;
53 
54  // `oracle` points to an object that has an `sfPriceDataSeries` field.
55  // When this function is called, that is a `PriceOracle` ledger object,
56  // but after one iteration of the loop below, it is an `sfNewFields`
57  // / `sfFinalFields` object in a `CreatedNode` / `ModifiedNode` object in
58  // a transaction's metadata.
59 
60  // `chain` points to an object that has `sfPreviousTxnID` and
61  // `sfPreviousTxnLgrSeq` fields. When this function is called,
62  // that is the `PriceOracle` ledger object pointed to by `oracle`,
63  // but after one iteration of the loop below, then it is a `ModifiedNode`
64  // / `CreatedNode` object in a transaction's metadata.
65  STObject const* oracle = sle.get();
66  STObject const* chain = oracle;
67  // Use to test an unlikely scenario when CreatedNode / ModifiedNode
68  // for the Oracle is not found in the inner loop
69  STObject const* prevChain = nullptr;
70 
71  Meta meta = nullptr;
72  while (true)
73  {
74  if (prevChain == chain)
75  return;
76 
77  if (!oracle || f(*oracle) || isNew)
78  return;
79 
80  if (++history > maxHistory)
81  return;
82 
83  uint256 prevTx = chain->getFieldH256(sfPreviousTxnID);
85 
86  auto const ledger = context.ledgerMaster.getLedgerBySeq(prevSeq);
87  if (!ledger)
88  return;
89 
90  meta = ledger->txRead(prevTx).second;
91 
92  for (STObject const& node : meta->getFieldArray(sfAffectedNodes))
93  {
94  if (node.getFieldU16(sfLedgerEntryType) != ltORACLE)
95  {
96  continue;
97  }
98 
99  prevChain = chain;
100  chain = &node;
101  isNew = node.isFieldPresent(sfNewFields);
102  // if a meta is for the new and this is the first
103  // look-up then it's the meta for the tx that
104  // created the current object; i.e. there is no
105  // historical data
106  if (isNew && history == 1)
107  return;
108 
109  oracle = isNew
110  ? &static_cast<const STObject&>(node.peekAtField(sfNewFields))
111  : &static_cast<const STObject&>(
112  node.peekAtField(sfFinalFields));
113  break;
114  }
115  }
116 }
117 
118 // Return avg, sd, data set size
121  Prices::right_const_iterator const& begin,
122  Prices::right_const_iterator const& end)
123 {
124  STAmount avg{noIssue(), 0, 0};
125  Number sd{0};
126  std::uint16_t const size = std::distance(begin, end);
127  avg = std::accumulate(
128  begin, end, avg, [&](STAmount const& acc, auto const& it) {
129  return acc + it.first;
130  });
131  avg = divide(avg, STAmount{noIssue(), size, 0}, noIssue());
132  if (size > 1)
133  {
134  sd = std::accumulate(
135  begin, end, sd, [&](Number const& acc, auto const& it) {
136  return acc + (it.first - avg) * (it.first - avg);
137  });
138  sd = root2(sd / (size - 1));
139  }
140  return {avg, sd, size};
141 };
142 
153 {
154  Json::Value result;
155  auto const& params(context.params);
156 
157  constexpr std::uint16_t maxOracles = 200;
158  if (!params.isMember(jss::oracles))
159  return RPC::missing_field_error(jss::oracles);
160  if (!params[jss::oracles].isArray() || params[jss::oracles].size() == 0 ||
161  params[jss::oracles].size() > maxOracles)
162  {
164  return result;
165  }
166 
167  if (!params.isMember(jss::base_asset))
168  return RPC::missing_field_error(jss::base_asset);
169 
170  if (!params.isMember(jss::quote_asset))
171  return RPC::missing_field_error(jss::quote_asset);
172 
173  // Lambda to get `trim` and `time_threshold` fields. If the field
174  // is not included in the input then a default value is returned.
175  auto getField = [&params](
176  Json::StaticString const& field,
177  unsigned int def =
179  if (params.isMember(field))
180  {
181  if (!params[field].isConvertibleTo(Json::ValueType::uintValue))
182  return rpcORACLE_MALFORMED;
183  return params[field].asUInt();
184  }
185  return def;
186  };
187 
188  auto const trim = getField(jss::trim);
189  if (std::holds_alternative<error_code_i>(trim))
190  {
191  RPC::inject_error(std::get<error_code_i>(trim), result);
192  return result;
193  }
194  if (params.isMember(jss::trim) &&
195  (std::get<std::uint32_t>(trim) == 0 ||
196  std::get<std::uint32_t>(trim) > maxTrim))
197  {
199  return result;
200  }
201 
202  auto const timeThreshold = getField(jss::time_threshold, 0);
203  if (std::holds_alternative<error_code_i>(timeThreshold))
204  {
205  RPC::inject_error(std::get<error_code_i>(timeThreshold), result);
206  return result;
207  }
208 
209  auto const& baseAsset = params[jss::base_asset];
210  auto const& quoteAsset = params[jss::quote_asset];
211 
212  // Collect the dataset into bimap keyed by lastUpdateTime and
213  // STAmount (Number is int64 and price is uint64)
214  Prices prices;
215  for (auto const& oracle : params[jss::oracles])
216  {
217  if (!oracle.isMember(jss::oracle_document_id) ||
218  !oracle.isMember(jss::account))
219  {
221  return result;
222  }
223  auto const documentID = oracle[jss::oracle_document_id].isConvertibleTo(
224  Json::ValueType::uintValue)
225  ? std::make_optional(oracle[jss::oracle_document_id].asUInt())
226  : std::nullopt;
227  auto const account =
228  parseBase58<AccountID>(oracle[jss::account].asString());
229  if (!account || account->isZero() || !documentID)
230  {
232  return result;
233  }
234 
236  result = RPC::lookupLedger(ledger, context);
237  if (!ledger)
238  return result;
239 
240  auto const sle = ledger->read(keylet::oracle(*account, *documentID));
241  iteratePriceData(context, sle, [&](STObject const& node) {
242  auto const& series = node.getFieldArray(sfPriceDataSeries);
243  // find the token pair entry with the price
244  if (auto iter = std::find_if(
245  series.begin(),
246  series.end(),
247  [&](STObject const& o) -> bool {
248  return o.getFieldCurrency(sfBaseAsset).getText() ==
249  baseAsset &&
250  o.getFieldCurrency(sfQuoteAsset).getText() ==
251  quoteAsset &&
252  o.isFieldPresent(sfAssetPrice);
253  });
254  iter != series.end())
255  {
256  auto const price = iter->getFieldU64(sfAssetPrice);
257  auto const scale = iter->isFieldPresent(sfScale)
258  ? -static_cast<int>(iter->getFieldU8(sfScale))
259  : 0;
260  prices.insert(Prices::value_type(
261  node.getFieldU32(sfLastUpdateTime),
262  STAmount{noIssue(), price, scale}));
263  return true;
264  }
265  return false;
266  });
267  }
268 
269  if (prices.empty())
270  {
272  return result;
273  }
274 
275  // erase outdated data
276  // sorted in descending, therefore begin is the latest, end is the oldest
277  auto const latestTime = prices.left.begin()->first;
278  if (auto const threshold = std::get<std::uint32_t>(timeThreshold))
279  {
280  // threshold defines an acceptable range {max,min} of lastUpdateTime as
281  // {latestTime, latestTime - threshold}, the prices with lastUpdateTime
282  // greater than (latestTime - threshold) are erased.
283  auto const oldestTime = prices.left.rbegin()->first;
284  auto const upperBound =
285  latestTime > threshold ? (latestTime - threshold) : oldestTime;
286  if (upperBound > oldestTime)
287  prices.left.erase(
288  prices.left.upper_bound(upperBound), prices.left.end());
289 
290  if (prices.empty())
291  {
293  return result;
294  }
295  }
296  result[jss::time] = latestTime;
297 
298  // calculate stats
299  auto const [avg, sd, size] =
300  getStats(prices.right.begin(), prices.right.end());
301  result[jss::entire_set][jss::mean] = avg.getText();
302  result[jss::entire_set][jss::size] = size;
303  result[jss::entire_set][jss::standard_deviation] = to_string(sd);
304 
305  auto itAdvance = [&](auto it, int distance) {
306  std::advance(it, distance);
307  return it;
308  };
309 
310  auto const median = [&prices, &itAdvance, &size_ = size]() {
311  auto const middle = size_ / 2;
312  if ((size_ % 2) == 0)
313  {
314  static STAmount two{noIssue(), 2, 0};
315  auto it = itAdvance(prices.right.begin(), middle - 1);
316  auto const& a1 = it->first;
317  auto const& a2 = (++it)->first;
318  return divide(a1 + a2, two, noIssue());
319  }
320  return itAdvance(prices.right.begin(), middle)->first;
321  }();
322  result[jss::median] = median.getText();
323 
324  if (std::get<std::uint32_t>(trim) != 0)
325  {
326  auto const trimCount =
327  prices.size() * std::get<std::uint32_t>(trim) / 100;
328 
329  auto const [avg, sd, size] = getStats(
330  itAdvance(prices.right.begin(), trimCount),
331  itAdvance(prices.right.end(), -trimCount));
332  result[jss::trimmed_set][jss::mean] = avg.getText();
333  result[jss::trimmed_set][jss::size] = size;
334  result[jss::trimmed_set][jss::standard_deviation] = to_string(sd);
335  }
336 
337  return result;
338 }
339 
340 } // namespace ripple
ripple::sfPreviousTxnLgrSeq
const SF_UINT32 sfPreviousTxnLgrSeq
ripple::STObject::getFieldArray
const STArray & getFieldArray(SField const &field) const
Definition: STObject.cpp:640
ripple::RPC::JsonContext
Definition: Context.h:53
ripple::root2
Number root2(Number f)
Definition: Number.cpp:689
std::shared_ptr
STL class.
ripple::rpcINVALID_PARAMS
@ rpcINVALID_PARAMS
Definition: ErrorCodes.h:84
ripple::ltORACLE
@ ltORACLE
A ledger object which tracks Oracle.
Definition: LedgerFormats.h:198
ripple::iteratePriceData
static void iteratePriceData(RPC::JsonContext &context, std::shared_ptr< SLE const > const &sle, std::function< bool(STObject const &)> &&f)
Calls callback "f" on the ledger-object sle and up to three previous metadata objects.
Definition: GetAggregatePrice.cpp:44
ripple::keylet::oracle
Keylet oracle(AccountID const &account, std::uint32_t const &documentID) noexcept
Definition: Indexes.cpp:449
std::find_if
T find_if(T... args)
std::size
T size(T... args)
std::make_optional
T make_optional(T... args)
ripple::RPC::Context::ledgerMaster
LedgerMaster & ledgerMaster
Definition: Context.h:45
std::shared_ptr::get
T get(T... args)
std::distance
T distance(T... args)
ripple::sfFinalFields
const SField sfFinalFields
ripple::noIssue
Issue const & noIssue()
Returns an asset specifier that represents no account and currency.
Definition: Issue.h:113
std::tuple
std::function
ripple::RPC::lookupLedger
Status lookupLedger(std::shared_ptr< ReadView const > &ledger, JsonContext &context, Json::Value &result)
Look up a ledger from a request and fill a Json::Result with the data representing a ledger.
Definition: RPCHelpers.cpp:623
ripple::rpcOBJECT_NOT_FOUND
@ rpcOBJECT_NOT_FOUND
Definition: ErrorCodes.h:143
ripple::rpcORACLE_MALFORMED
@ rpcORACLE_MALFORMED
Definition: ErrorCodes.h:149
ripple::RPC::missing_field_error
Json::Value missing_field_error(std::string const &name)
Definition: ErrorCodes.h:268
ripple::divide
STAmount divide(STAmount const &amount, Rate const &rate)
Definition: Rate2.cpp:86
ripple::base_uint< 256 >
ripple::Number
Definition: Number.h:36
ripple::sfNewFields
const SField sfNewFields
ripple::sfAffectedNodes
const SField sfAffectedNodes
ripple::STAmount
Definition: STAmount.h:46
ripple::LedgerMaster::getLedgerBySeq
std::shared_ptr< Ledger const > getLedgerBySeq(std::uint32_t index)
Definition: LedgerMaster.cpp:1871
std::accumulate
T accumulate(T... args)
ripple::sfPreviousTxnID
const SF_UINT256 sfPreviousTxnID
std::uint8_t
std::advance
T advance(T... args)
ripple::STObject
Definition: STObject.h:55
ripple
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition: RCLCensorshipDetector.h:29
ripple::sfLedgerEntryType
const SF_UINT16 sfLedgerEntryType
Json::StaticString
Lightweight wrapper to tag static string.
Definition: json_value.h:60
ripple::STObject::isFieldPresent
bool isFieldPresent(SField const &field) const
Definition: STObject.cpp:444
ripple::doGetAggregatePrice
Json::Value doGetAggregatePrice(RPC::JsonContext &context)
oracles: array of {account, oracle_document_id} base_asset: is the asset to be priced quote_asset: is...
Definition: GetAggregatePrice.cpp:152
ripple::to_string
std::string to_string(Manifest const &m)
Format the specified manifest to a string for debugging purposes.
Definition: app/misc/impl/Manifest.cpp:38
ripple::Prices
bimap< multiset_of< std::uint32_t, std::greater< std::uint32_t > >, multiset_of< STAmount > > Prices
Definition: GetAggregatePrice.cpp:38
ripple::STObject::getFieldU32
std::uint32_t getFieldU32(SField const &field) const
Definition: STObject.cpp:575
ripple::maxTrim
constexpr std::size_t maxTrim
The maximum percentage of outliers to trim.
Definition: Protocol.h:135
ripple::RPC::JsonContext::params
Json::Value params
Definition: Context.h:64
ripple::sfPriceDataSeries
const SField sfPriceDataSeries
ripple::RPC::inject_error
void inject_error(error_code_i code, JsonValue &json)
Add or update the json update to reflect the error code.
Definition: ErrorCodes.h:218
ripple::getStats
static std::tuple< STAmount, Number, std::uint16_t > getStats(Prices::right_const_iterator const &begin, Prices::right_const_iterator const &end)
Definition: GetAggregatePrice.cpp:120
Json::Value
Represents a JSON value.
Definition: json_value.h:145
std::variant
ripple::STObject::getFieldH256
uint256 getFieldH256(SField const &field) const
Definition: STObject.cpp:599