xrpld
Loading...
Searching...
No Matches
GetAggregatePrice_test.cpp
1#include <test/jtx/Account.h>
2#include <test/jtx/Env.h>
3#include <test/jtx/Oracle.h>
4#include <test/jtx/amount.h>
5
6#include <xrpld/app/ledger/OpenLedger.h>
7
8#include <xrpl/basics/Number.h>
9#include <xrpl/basics/base_uint.h>
10#include <xrpl/beast/unit_test/suite.h>
11#include <xrpl/beast/utility/Journal.h>
12#include <xrpl/core/ServiceRegistry.h>
13#include <xrpl/ledger/OpenView.h>
14#include <xrpl/protocol/Feature.h>
15#include <xrpl/protocol/Indexes.h>
16#include <xrpl/protocol/SField.h>
17#include <xrpl/protocol/jss.h>
18
19#include <cstdlib>
20#include <memory>
21#include <optional>
22#include <string>
23#include <vector>
24
26
28{
29public:
30 void
32 {
33 testcase("Errors");
34 using namespace jtx;
35 Account const owner{"owner"};
36 Account const some{"some"};
37 static OraclesData kOracles = {{owner, 1}};
38
39 {
40 Env env(*this);
41 auto const baseFee = env.current()->fees().base;
42 // missing base_asset
43 auto ret = Oracle::aggregatePrice(env, std::nullopt, "USD", kOracles);
44 BEAST_EXPECT(ret[jss::error_message].asString() == "Missing field 'base_asset'.");
45
46 // missing quote_asset
47 ret = Oracle::aggregatePrice(env, "XRP", std::nullopt, kOracles);
48 BEAST_EXPECT(ret[jss::error_message].asString() == "Missing field 'quote_asset'.");
49
50 // invalid base_asset, quote_asset
51 std::vector<AnyValue> const invalidAsset = {
53 1,
54 -1,
55 1.2,
56 "",
57 "invalid",
58 "a",
59 "ab",
60 "A",
61 "AB",
62 "ABCD",
63 "010101",
64 "012345678901234567890123456789012345678",
65 "012345678901234567890123456789012345678G"};
66 for (auto const& v : invalidAsset)
67 {
68 ret = Oracle::aggregatePrice(env, "USD", v, kOracles);
69 BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
70 ret = Oracle::aggregatePrice(env, v, "USD", kOracles);
71 BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
72 ret = Oracle::aggregatePrice(env, v, v, kOracles);
73 BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
74 }
75
76 // missing oracles array
77 ret = Oracle::aggregatePrice(env, "XRP", "USD");
78 BEAST_EXPECT(ret[jss::error_message].asString() == "Missing field 'oracles'.");
79
80 // empty oracles array
81 ret = Oracle::aggregatePrice(env, "XRP", "USD", OraclesData{});
82 BEAST_EXPECT(ret[jss::error].asString() == "oracleMalformed");
83
84 // no token pairs found
85 ret = Oracle::aggregatePrice(env, "YAN", "USD", kOracles);
86 BEAST_EXPECT(ret[jss::error].asString() == "objectNotFound");
87
88 // invalid oracle document id
89 // id doesn't exist
90 ret = Oracle::aggregatePrice(env, "XRP", "USD", {{{owner, 2}}});
91 BEAST_EXPECT(ret[jss::error].asString() == "objectNotFound");
92 // invalid values
93 std::vector<AnyValue> const invalidDocument = {kNoneTag, 1.2, -1, "", "none", "1.2"};
94 for (auto const& v : invalidDocument)
95 {
96 ret = Oracle::aggregatePrice(env, "XRP", "USD", {{{owner, v}}});
97 json::Value jv;
98 toJson(jv, v);
99 BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
100 }
101 // missing document id
102 ret = Oracle::aggregatePrice(env, "XRP", "USD", {{{owner, std::nullopt}}});
103 BEAST_EXPECT(ret[jss::error].asString() == "oracleMalformed");
104
105 // invalid owner
106 ret = Oracle::aggregatePrice(env, "XRP", "USD", {{{some, 1}}});
107 BEAST_EXPECT(ret[jss::error].asString() == "objectNotFound");
108 // missing account
109 ret = Oracle::aggregatePrice(env, "XRP", "USD", {{{std::nullopt, 1}}});
110 BEAST_EXPECT(ret[jss::error].asString() == "oracleMalformed");
111
112 // oracles have wrong asset pair
113 env.fund(XRP(1'000), owner);
114 Oracle const oracle(
115 env,
116 {.owner = owner,
117 .series = {{"XRP", "EUR", 740, 1}},
118 .fee = static_cast<int>(baseFee.drops())});
119 ret = Oracle::aggregatePrice(env, "XRP", "USD", {{{owner, oracle.documentID()}}});
120 BEAST_EXPECT(ret[jss::error].asString() == "objectNotFound");
121
122 // invalid trim value
123 std::vector<AnyValue> const invalidTrim = {kNoneTag, 0, 26, -1, 1.2, "", "none", "1.2"};
124 for (auto const& v : invalidTrim)
125 {
126 ret =
127 Oracle::aggregatePrice(env, "XRP", "USD", {{{owner, oracle.documentID()}}}, v);
128 BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
129 }
130
131 // invalid time threshold value
132 std::vector<AnyValue> const invalidTime = {kNoneTag, -1, 1.2, "", "none", "1.2"};
133 for (auto const& v : invalidTime)
134 {
136 env, "XRP", "USD", {{{owner, oracle.documentID()}}}, std::nullopt, v);
137 BEAST_EXPECT(ret[jss::error].asString() == "invalidParams");
138 }
139 }
140
141 // too many oracles
142 {
143 Env env(*this);
144 auto const baseFee = static_cast<int>(env.current()->fees().base.drops());
145
146 OraclesData oracles;
147 for (int i = 0; i < 201; ++i)
148 {
149 Account const owner(std::to_string(i));
150 env.fund(XRP(1'000), owner);
151 Oracle const oracle(env, {.owner = owner, .documentID = i, .fee = baseFee});
152 oracles.emplace_back(owner, oracle.documentID());
153 }
154 auto const ret = Oracle::aggregatePrice(env, "XRP", "USD", oracles);
155 BEAST_EXPECT(ret[jss::error].asString() == "oracleMalformed");
156 }
157 }
158
159 void
161 {
162 testcase("RPC");
163 using namespace jtx;
164
165 auto prep = [&](Env& env, auto& oracles) {
166 oracles.reserve(10);
167 for (int i = 0; i < 10; ++i)
168 {
169 auto const baseFee = static_cast<int>(env.current()->fees().base.drops());
170
171 Account const owner{std::to_string(i)};
172 env.fund(XRP(1'000), owner);
173 Oracle const oracle(
174 env,
175 {.owner = owner,
176 .documentID = rand(),
177 .series = {{"XRP", "USD", 740 + i, 1}, {"XRP", "EUR", 740, 1}},
178 .fee = baseFee});
179 oracles.emplace_back(owner, oracle.documentID());
180 }
181 };
182
183 // Aggregate data set includes all price oracle instances, no trimming
184 // or time threshold
185 {
186 auto const all = testableAmendments();
187 for (auto const& feats : {all - featureSingleAssetVault - featureLendingProtocol, all})
188 {
189 for (auto const mantissaSize : MantissaRange::getAllScales())
190 {
191 // Regardless of the features enabled, RPC is controlled by
192 // the global mantissa size. And since it's a thread-local,
193 // overriding it locally won't make a difference either.
194 // This will mean all RPC will use the default of "large".
195 NumberMantissaScaleGuard const mg(mantissaSize);
196
197 Env env(*this, feats);
198 OraclesData oracles;
199 prep(env, oracles);
200 // entire and trimmed stats
201 auto ret = Oracle::aggregatePrice(env, "XRP", "USD", oracles);
202 BEAST_EXPECT(ret[jss::entire_set][jss::mean] == "74.45");
203 BEAST_EXPECT(ret[jss::entire_set][jss::size].asUInt() == 10);
204 // Short: 0.3027650354097492
205 BEAST_EXPECTS(
206 ret[jss::entire_set][jss::standard_deviation] == "0.3027650354097491666",
207 ret[jss::entire_set][jss::standard_deviation].asString());
208 BEAST_EXPECT(ret[jss::median] == "74.45");
209 BEAST_EXPECT(ret[jss::time] == 946694900);
210 }
211 }
212 }
213
214 // Aggregate data set includes all price oracle instances
215 {
216 Env env(*this);
217 OraclesData oracles;
218 prep(env, oracles);
219 // entire and trimmed stats
220 auto ret = Oracle::aggregatePrice(env, "XRP", "USD", oracles, 20, 100);
221 BEAST_EXPECT(ret[jss::entire_set][jss::mean] == "74.45");
222 BEAST_EXPECT(ret[jss::entire_set][jss::size].asUInt() == 10);
223 // Short: "0.3027650354097492",
224 BEAST_EXPECTS(
225 ret[jss::entire_set][jss::standard_deviation] == "0.3027650354097491666",
226 ret[jss::entire_set][jss::standard_deviation].asString());
227 BEAST_EXPECT(ret[jss::median] == "74.45");
228 BEAST_EXPECT(ret[jss::trimmed_set][jss::mean] == "74.45");
229 BEAST_EXPECT(ret[jss::trimmed_set][jss::size].asUInt() == 6);
230 // Short: "0.187082869338697",
231 BEAST_EXPECTS(
232 ret[jss::trimmed_set][jss::standard_deviation] == "0.1870828693386970693",
233 ret[jss::trimmed_set][jss::standard_deviation].asString());
234 BEAST_EXPECT(ret[jss::time] == 946694900);
235 }
236
237 // A reduced dataset, as some price oracles have data beyond three
238 // updated ledgers
239 {
240 Env env(*this);
241 auto const baseFee = static_cast<int>(env.current()->fees().base.drops());
242
243 OraclesData oracles;
244 prep(env, oracles);
245 for (int i = 0; i < 3; ++i)
246 {
248 env,
249 {.owner = oracles[i].first,
250 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
251 .documentID = asUInt(*oracles[i].second),
252 .fee = baseFee},
253 false);
254 // push XRP/USD by more than three ledgers, so this price
255 // oracle is not included in the dataset
256 oracle.set(UpdateArg{.series = {{"XRP", "EUR", 740, 1}}, .fee = baseFee});
257 oracle.set(UpdateArg{.series = {{"XRP", "EUR", 740, 1}}, .fee = baseFee});
258 oracle.set(UpdateArg{.series = {{"XRP", "EUR", 740, 1}}, .fee = baseFee});
259 }
260 for (int i = 3; i < 6; ++i)
261 {
263 env,
264 {.owner = oracles[i].first,
265 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
266 .documentID = asUInt(*oracles[i].second),
267 .fee = baseFee},
268 false);
269 // push XRP/USD by two ledgers, so this price
270 // is included in the dataset
271 oracle.set(UpdateArg{.series = {{"XRP", "EUR", 740, 1}}, .fee = baseFee});
272 oracle.set(UpdateArg{.series = {{"XRP", "EUR", 740, 1}}, .fee = baseFee});
273 }
274
275 // entire and trimmed stats
276 auto ret = Oracle::aggregatePrice(env, "XRP", "USD", oracles, 20, "200");
277 BEAST_EXPECT(ret[jss::entire_set][jss::mean] == "74.6");
278 BEAST_EXPECT(ret[jss::entire_set][jss::size].asUInt() == 7);
279 // Short: 0.2160246899469287
280 BEAST_EXPECTS(
281 ret[jss::entire_set][jss::standard_deviation] == "0.2160246899469286744",
282 ret[jss::entire_set][jss::standard_deviation].asString());
283 BEAST_EXPECT(ret[jss::median] == "74.6");
284 BEAST_EXPECT(ret[jss::trimmed_set][jss::mean] == "74.6");
285 BEAST_EXPECT(ret[jss::trimmed_set][jss::size].asUInt() == 5);
286 // Short: 0.158113883008419
287 BEAST_EXPECTS(
288 ret[jss::trimmed_set][jss::standard_deviation] == "0.1581138830084189666",
289 ret[jss::trimmed_set][jss::standard_deviation].asString());
290 BEAST_EXPECT(ret[jss::time] == 946694900);
291 }
292
293 // Reduced data set because of the time threshold
294 {
295 Env env(*this);
296 auto const baseFee = static_cast<int>(env.current()->fees().base.drops());
297
298 OraclesData oracles;
299 prep(env, oracles);
300 for (int i = 0; i < oracles.size(); ++i)
301 {
303 env,
304 {.owner = oracles[i].first,
305 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
306 .documentID = asUInt(*oracles[i].second),
307 .fee = baseFee},
308 false);
309 // push XRP/USD by two ledgers, so this price
310 // is included in the dataset
311 oracle.set(UpdateArg{.series = {{"XRP", "USD", 740, 1}}, .fee = baseFee});
312 }
313
314 // entire stats only, limit lastUpdateTime to {200, 125}
315 auto ret = Oracle::aggregatePrice(env, "XRP", "USD", oracles, std::nullopt, 75);
316 BEAST_EXPECT(ret[jss::entire_set][jss::mean] == "74");
317 BEAST_EXPECT(ret[jss::entire_set][jss::size].asUInt() == 8);
318 BEAST_EXPECT(ret[jss::entire_set][jss::standard_deviation] == "0");
319 BEAST_EXPECT(ret[jss::median] == "74");
320 BEAST_EXPECT(ret[jss::time] == 946695000);
321 }
322 }
323
324 void
326 {
327 testcase("Null txRead metadata");
328 using namespace jtx;
329
330 // Verify that iteratePriceData handles a null txRead result
331 // gracefully (returns early) rather than crashing with a
332 // nullptr dereference. This simulates local data corruption
333 // where a transaction referenced by sfPreviousTxnID is missing
334 // from the ledger's transaction map.
335 Env env(*this);
336 auto const baseFee = static_cast<int>(env.current()->fees().base.drops());
337
338 Account const owner{"owner"};
339 env.fund(XRP(1'000), owner);
340
341 // Create oracle with XRP/USD and XRP/EUR
343 env,
344 {.owner = owner,
345 .series = {{"XRP", "USD", 740, 1}, {"XRP", "EUR", 840, 1}},
346 .fee = baseFee});
347
348 // Update oracle to only have XRP/EUR, pushing XRP/USD into
349 // history. iteratePriceData will need to read historical tx
350 // metadata to find the XRP/USD price.
351 oracle.set(UpdateArg{.series = {{"XRP", "EUR", 850, 1}}, .fee = baseFee});
352
353 OraclesData const oracles{{owner, oracle.documentID()}};
354
355 // Precondition: with an uncorrupted oracle, the historical
356 // traversal must succeed and produce a price for XRP/USD.
357 // This proves the test reaches iteratePriceData's history
358 // path; without it, a future change that breaks the setup
359 // could turn the post-corruption assertion into a vacuous
360 // pass (objectNotFound is reachable from many unrelated
361 // code paths).
362 {
363 auto const ret = Oracle::aggregatePrice(env, "XRP", "USD", oracles);
364 BEAST_EXPECT(!ret.isMember(jss::error));
365 BEAST_EXPECT(ret.isMember(jss::median));
366 }
367
368 // Simulate data corruption: modify the oracle SLE in the open
369 // ledger to have a bogus sfPreviousTxnID that doesn't exist in
370 // any ledger. sfPreviousTxnLgrSeq still points to a valid closed
371 // ledger, so getLedgerBySeq succeeds but txRead returns null.
372 auto const oracleKeylet = keylet::oracle(owner, oracle.documentID());
373 uint256 const bogusTxnID{0xABCABCAB};
374 bool const modified = env.app().getOpenLedger().modify(
375 [&oracleKeylet, &bogusTxnID](OpenView& view, beast::Journal) -> bool {
376 auto const sle = view.read(oracleKeylet);
377 if (!sle)
378 return false;
379 auto replacement = std::make_shared<SLE>(*sle, sle->key());
380 replacement->setFieldH256(sfPreviousTxnID, bogusTxnID);
381 view.rawReplace(replacement);
382 return true;
383 });
384
385 // Confirm the injection actually took effect: modify must
386 // report success, and re-reading the SLE must show the
387 // bogus hash. Otherwise the failure-mode assertion below
388 // would not be exercising the null-txRead path at all.
389 BEAST_EXPECT(modified);
390 if (auto const sle = env.current()->read(oracleKeylet); BEAST_EXPECT(sle))
391 BEAST_EXPECT(sle->getFieldH256(sfPreviousTxnID) == bogusTxnID);
392
393 // Query for XRP/USD using the "current" (open) ledger.
394 // The oracle SLE now has a bogus sfPreviousTxnID. The current
395 // oracle only has EUR, so iteratePriceData will try to read
396 // history. txRead returns null for the bogus hash, and the
397 // null check should cause a graceful early return instead of
398 // a nullptr dereference.
399 auto const ret = Oracle::aggregatePrice(env, "XRP", "USD", oracles);
400 BEAST_EXPECT(ret[jss::error].asString() == "objectNotFound");
401 }
402
403 void
404 run() override
405 {
406 testErrors();
407 testRpc();
409 }
410};
411
412BEAST_DEFINE_TESTSUITE(GetAggregatePrice, rpc, xrpl);
413
414} // namespace xrpl::test::jtx::oracle
A generic endpoint for log messages.
Definition Journal.h:38
A testsuite class.
Definition suite.h:50
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:149
Represents a JSON value.
Definition json_value.h:130
Sets the new scale and restores the old scale when it leaves scope.
Definition Number.h:920
bool modify(modify_type const &f)
Modify the open ledger.
Writable ledger view that accumulates state and tx changes.
Definition OpenView.h:45
SLE::const_pointer read(Keylet const &k) const override
Return the state item associated with a key.
Definition OpenView.cpp:167
void rawReplace(SLE::ref sle) override
Unconditionally replace a state item.
Definition OpenView.cpp:243
virtual OpenLedger & getOpenLedger()=0
Immutable cryptographic account descriptor.
Definition jtx/Account.h:17
A transaction testing environment.
Definition Env.h:143
Application & app()
Definition Env.h:280
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:296
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:353
Oracle class facilitates unit-testing of the Price Oracle feature.
static json::Value aggregatePrice(Env &env, std::optional< AnyValue > const &baseAsset, std::optional< AnyValue > const &quoteAsset, std::optional< OraclesData > const &oracles=std::nullopt, std::optional< AnyValue > const &trim=std::nullopt, std::optional< AnyValue > const &timeThreshold=std::nullopt)
Definition Oracle.cpp:161
T emplace_back(T... args)
T make_shared(T... args)
Keylet oracle(AccountID const &account, std::uint32_t const &documentID) noexcept
Definition Indexes.cpp:515
std::uint32_t asUInt(AnyValue const &v)
Definition Oracle.cpp:398
std::vector< std::pair< std::optional< Account >, std::optional< AnyValue > > > OraclesData
constexpr char const * kNoneTag
void toJson(json::Value &jv, AnyValue const &v)
Definition Oracle.cpp:368
BEAST_DEFINE_TESTSUITE(Oracle, app, xrpl)
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
FeatureBitset testableAmendments()
Definition Env.h:76
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
BaseUInt< 256 > uint256
Definition base_uint.h:562
T size(T... args)
static std::set< MantissaScale > const & getAllScales()
Definition Number.cpp:37
T to_string(T... args)