Skip to content

Commit f4da2e3

Browse files
gregtatcamseelabs
andauthored
Price Oracle: validate input parameters and extend test coverage: (XRPLF#5013)
* Price Oracle: validate input parameters and extend test coverage: Validate trim, time_threshold, document_id are valid Int, UInt, or string convertible to UInt. Validate base_asset and quote_asset are valid currency. Update error codes. Extend Oracle and GetAggregatePrice unit-tests. Denote unreachable coverage code. * Set one-line LCOV_EXCL_LINE * Move ledger_entry tests to LedgerRPC_test.cpp * Add constants for "None" * Fix LedgerRPC test --------- Co-authored-by: Scott Determan <[email protected]>
1 parent f650949 commit f4da2e3

File tree

9 files changed

+472
-157
lines changed

9 files changed

+472
-157
lines changed

src/ripple/app/tx/impl/DeleteOracle.cpp

+8-4
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ TER
4848
DeleteOracle::preclaim(PreclaimContext const& ctx)
4949
{
5050
if (!ctx.view.exists(keylet::account(ctx.tx.getAccountID(sfAccount))))
51-
return terNO_ACCOUNT;
51+
return terNO_ACCOUNT; // LCOV_EXCL_LINE
5252

5353
if (auto const sle = ctx.view.read(keylet::oracle(
5454
ctx.tx.getAccountID(sfAccount), ctx.tx[sfOracleDocumentID]));
@@ -60,8 +60,10 @@ DeleteOracle::preclaim(PreclaimContext const& ctx)
6060
else if (ctx.tx.getAccountID(sfAccount) != sle->getAccountID(sfOwner))
6161
{
6262
// this can't happen because of the above check
63+
// LCOV_EXCL_START
6364
JLOG(ctx.j.debug()) << "Oracle Delete: invalid account.";
6465
return tecINTERNAL;
66+
// LCOV_EXCL_STOP
6567
}
6668
return tesSUCCESS;
6769
}
@@ -74,18 +76,20 @@ DeleteOracle::deleteOracle(
7476
beast::Journal j)
7577
{
7678
if (!sle)
77-
return tesSUCCESS;
79+
return tecINTERNAL; // LCOV_EXCL_LINE
7880

7981
if (!view.dirRemove(
8082
keylet::ownerDir(account), (*sle)[sfOwnerNode], sle->key(), true))
8183
{
84+
// LCOV_EXCL_START
8285
JLOG(j.fatal()) << "Unable to delete Oracle from owner.";
8386
return tefBAD_LEDGER;
87+
// LCOV_EXCL_STOP
8488
}
8589

8690
auto const sleOwner = view.peek(keylet::account(account));
8791
if (!sleOwner)
88-
return tecINTERNAL;
92+
return tecINTERNAL; // LCOV_EXCL_LINE
8993

9094
auto const count =
9195
sle->getFieldArray(sfPriceDataSeries).size() > 5 ? -2 : -1;
@@ -104,7 +108,7 @@ DeleteOracle::doApply()
104108
keylet::oracle(account_, ctx_.tx[sfOracleDocumentID])))
105109
return deleteOracle(ctx_.view(), sle, account_, j_);
106110

107-
return tecINTERNAL;
111+
return tecINTERNAL; // LCOV_EXCL_LINE
108112
}
109113

110114
} // namespace ripple

src/ripple/app/tx/impl/SetOracle.cpp

+6-7
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ SetOracle::preclaim(PreclaimContext const& ctx)
7474
auto const sleSetter =
7575
ctx.view.read(keylet::account(ctx.tx.getAccountID(sfAccount)));
7676
if (!sleSetter)
77-
return terNO_ACCOUNT;
77+
return terNO_ACCOUNT; // LCOV_EXCL_LINE
7878

7979
// lastUpdateTime must be within maxLastUpdateTimeDelta seconds
8080
// of the last closed ledger
@@ -88,8 +88,7 @@ SetOracle::preclaim(PreclaimContext const& ctx)
8888
std::size_t const lastUpdateTimeEpoch =
8989
lastUpdateTime - epoch_offset.count();
9090
if (closeTime < maxLastUpdateTimeDelta)
91-
Throw<std::runtime_error>(
92-
"Oracle: close time is less than maxLastUpdateTimeDelta");
91+
return tecINTERNAL; // LCOV_EXCL_LINE
9392
if (lastUpdateTimeEpoch < (closeTime - maxLastUpdateTimeDelta) ||
9493
lastUpdateTimeEpoch > (closeTime + maxLastUpdateTimeDelta))
9594
return tecINVALID_UPDATE_TIME;
@@ -194,7 +193,7 @@ adjustOwnerCount(ApplyContext& ctx, int count)
194193
return true;
195194
}
196195

197-
return false;
196+
return false; // LCOV_EXCL_LINE
198197
}
199198

200199
static void
@@ -274,7 +273,7 @@ SetOracle::doApply()
274273
auto const newCount = pairs.size() > 5 ? 2 : 1;
275274
auto const adjust = newCount - oldCount;
276275
if (adjust != 0 && !adjustOwnerCount(ctx_, adjust))
277-
return tefINTERNAL;
276+
return tefINTERNAL; // LCOV_EXCL_LINE
278277

279278
ctx_.view().update(sle);
280279
}
@@ -295,13 +294,13 @@ SetOracle::doApply()
295294
auto page = ctx_.view().dirInsert(
296295
keylet::ownerDir(account_), sle->key(), describeOwnerDir(account_));
297296
if (!page)
298-
return tecDIR_FULL;
297+
return tecDIR_FULL; // LCOV_EXCL_LINE
299298

300299
(*sle)[sfOwnerNode] = *page;
301300

302301
auto const count = series.size() > 5 ? 2 : 1;
303302
if (!adjustOwnerCount(ctx_, count))
304-
return tefINTERNAL;
303+
return tefINTERNAL; // LCOV_EXCL_LINE
305304

306305
ctx_.view().insert(sle);
307306
}

src/ripple/rpc/handlers/GetAggregatePrice.cpp

+55-13
Original file line numberDiff line numberDiff line change
@@ -85,18 +85,18 @@ iteratePriceData(
8585

8686
auto const ledger = context.ledgerMaster.getLedgerBySeq(prevSeq);
8787
if (!ledger)
88-
return;
88+
return; // LCOV_EXCL_LINE
8989

9090
meta = ledger->txRead(prevTx).second;
9191

92+
prevChain = chain;
9293
for (STObject const& node : meta->getFieldArray(sfAffectedNodes))
9394
{
9495
if (node.getFieldU16(sfLedgerEntryType) != ltORACLE)
9596
{
9697
continue;
9798
}
9899

99-
prevChain = chain;
100100
chain = &node;
101101
isNew = node.isFieldPresent(sfNewFields);
102102
// if a meta is for the new and this is the first
@@ -170,21 +170,49 @@ doGetAggregatePrice(RPC::JsonContext& context)
170170
if (!params.isMember(jss::quote_asset))
171171
return RPC::missing_field_error(jss::quote_asset);
172172

173+
// Lambda to validate uint type
174+
// support positive int, uint, and a number represented as a string
175+
auto validUInt = [](Json::Value const& params,
176+
Json::StaticString const& field) {
177+
auto const& jv = params[field];
178+
std::uint32_t v;
179+
return jv.isUInt() || (jv.isInt() && jv.asInt() >= 0) ||
180+
(jv.isString() && beast::lexicalCastChecked(v, jv.asString()));
181+
};
182+
173183
// Lambda to get `trim` and `time_threshold` fields. If the field
174184
// is not included in the input then a default value is returned.
175-
auto getField = [&params](
185+
auto getField = [&params, &validUInt](
176186
Json::StaticString const& field,
177187
unsigned int def =
178188
0) -> std::variant<std::uint32_t, error_code_i> {
179189
if (params.isMember(field))
180190
{
181-
if (!params[field].isConvertibleTo(Json::ValueType::uintValue))
182-
return rpcORACLE_MALFORMED;
191+
if (!validUInt(params, field))
192+
return rpcINVALID_PARAMS;
183193
return params[field].asUInt();
184194
}
185195
return def;
186196
};
187197

198+
// Lambda to get `base_asset` and `quote_asset`. The values have
199+
// to conform to the Currency type.
200+
auto getCurrency =
201+
[&params](SField const& sField, Json::StaticString const& field)
202+
-> std::variant<Json::Value, error_code_i> {
203+
try
204+
{
205+
if (params[field].asString().empty())
206+
return rpcINVALID_PARAMS;
207+
currencyFromJson(sField, params[field]);
208+
return params[field];
209+
}
210+
catch (...)
211+
{
212+
return rpcINVALID_PARAMS;
213+
}
214+
};
215+
188216
auto const trim = getField(jss::trim);
189217
if (std::holds_alternative<error_code_i>(trim))
190218
{
@@ -206,8 +234,18 @@ doGetAggregatePrice(RPC::JsonContext& context)
206234
return result;
207235
}
208236

209-
auto const& baseAsset = params[jss::base_asset];
210-
auto const& quoteAsset = params[jss::quote_asset];
237+
auto const baseAsset = getCurrency(sfBaseAsset, jss::base_asset);
238+
if (std::holds_alternative<error_code_i>(baseAsset))
239+
{
240+
RPC::inject_error(std::get<error_code_i>(baseAsset), result);
241+
return result;
242+
}
243+
auto const quoteAsset = getCurrency(sfQuoteAsset, jss::quote_asset);
244+
if (std::holds_alternative<error_code_i>(quoteAsset))
245+
{
246+
RPC::inject_error(std::get<error_code_i>(quoteAsset), result);
247+
return result;
248+
}
211249

212250
// Collect the dataset into bimap keyed by lastUpdateTime and
213251
// STAmount (Number is int64 and price is uint64)
@@ -220,8 +258,7 @@ doGetAggregatePrice(RPC::JsonContext& context)
220258
RPC::inject_error(rpcORACLE_MALFORMED, result);
221259
return result;
222260
}
223-
auto const documentID = oracle[jss::oracle_document_id].isConvertibleTo(
224-
Json::ValueType::uintValue)
261+
auto const documentID = validUInt(oracle, jss::oracle_document_id)
225262
? std::make_optional(oracle[jss::oracle_document_id].asUInt())
226263
: std::nullopt;
227264
auto const account =
@@ -235,7 +272,7 @@ doGetAggregatePrice(RPC::JsonContext& context)
235272
std::shared_ptr<ReadView const> ledger;
236273
result = RPC::lookupLedger(ledger, context);
237274
if (!ledger)
238-
return result;
275+
return result; // LCOV_EXCL_LINE
239276

240277
auto const sle = ledger->read(keylet::oracle(*account, *documentID));
241278
iteratePriceData(context, sle, [&](STObject const& node) {
@@ -246,9 +283,9 @@ doGetAggregatePrice(RPC::JsonContext& context)
246283
series.end(),
247284
[&](STObject const& o) -> bool {
248285
return o.getFieldCurrency(sfBaseAsset).getText() ==
249-
baseAsset &&
286+
std::get<Json::Value>(baseAsset) &&
250287
o.getFieldCurrency(sfQuoteAsset).getText() ==
251-
quoteAsset &&
288+
std::get<Json::Value>(quoteAsset) &&
252289
o.isFieldPresent(sfAssetPrice);
253290
});
254291
iter != series.end())
@@ -287,10 +324,15 @@ doGetAggregatePrice(RPC::JsonContext& context)
287324
prices.left.erase(
288325
prices.left.upper_bound(upperBound), prices.left.end());
289326

327+
// At least one element should remain since upperBound is either
328+
// equal to oldestTime or is less than latestTime, in which case
329+
// the data is deleted between the oldestTime and upperBound.
290330
if (prices.empty())
291331
{
292-
RPC::inject_error(rpcOBJECT_NOT_FOUND, result);
332+
// LCOV_EXCL_START
333+
RPC::inject_error(rpcINTERNAL, result);
293334
return result;
335+
// LCOV_EXCL_STOP
294336
}
295337
}
296338
result[jss::time] = latestTime;

src/ripple/rpc/handlers/LedgerEntry.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -624,7 +624,7 @@ doLedgerEntry(RPC::JsonContext& context)
624624
auto const& oracle = context.params[jss::oracle];
625625
auto const documentID = [&]() -> std::optional<std::uint32_t> {
626626
auto const& id = oracle[jss::oracle_document_id];
627-
if (id.isConvertibleTo(Json::ValueType::uintValue))
627+
if (id.isUInt() || (id.isInt() && id.asInt() >= 0))
628628
return std::make_optional(id.asUInt());
629629
else if (id.isString())
630630
{

0 commit comments

Comments
 (0)