Skip to content

Commit

Permalink
Enforce account RPC limits by account objects traversed
Browse files Browse the repository at this point in the history
  • Loading branch information
natenichols committed Dec 16, 2021
1 parent 915fe31 commit f0cc98f
Show file tree
Hide file tree
Showing 9 changed files with 351 additions and 148 deletions.
83 changes: 54 additions & 29 deletions src/ripple/rpc/handlers/AccountChannels.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ doAccountChannels(RPC::JsonContext& context)
if (auto err = readLimitField(limit, RPC::Tuning::accountChannels, context))
return *err;

if (limit == 0u)
return rpcError(rpcINVALID_PARAMS);

Json::Value jsonChannels{Json::arrayValue};
struct VisitData
{
Expand All @@ -110,71 +113,93 @@ doAccountChannels(RPC::JsonContext& context)
AccountID const& raDstAccount;
};
VisitData visitData = {{}, accountID, hasDst, raDstAccount};
visitData.items.reserve(limit + 1);
uint256 startAfter;
std::uint64_t startHint;
visitData.items.reserve(limit);
uint256 startAfter = beast::zero;
std::uint64_t startHint = 0;

if (params.isMember(jss::marker))
{
Json::Value const& marker(params[jss::marker]);

if (!marker.isString())
if (!params[jss::marker].isString())
return RPC::expected_field_error(jss::marker, "string");

if (!startAfter.parseHex(marker.asString()))
{
// Marker is composed of a comma separated index and start hint. The
// former will be read as hex, and the latter using boost lexical cast.
std::stringstream marker(params[jss::marker].asString());
std::string value;
if (!std::getline(marker, value, ','))
return rpcError(rpcINVALID_PARAMS);
}

auto const sleChannel = ledger->read({ltPAYCHAN, startAfter});
if (!startAfter.parseHex(value))
return rpcError(rpcINVALID_PARAMS);

if (!sleChannel)
if (!std::getline(marker, value, ','))
return rpcError(rpcINVALID_PARAMS);

if (!visitData.hasDst ||
visitData.raDstAccount == (*sleChannel)[sfDestination])
try
{
visitData.items.emplace_back(sleChannel);
startHint = sleChannel->getFieldU64(sfOwnerNode);
startHint = boost::lexical_cast<std::uint64_t>(value);
}
else
catch (boost::bad_lexical_cast&)
{
return rpcError(rpcINVALID_PARAMS);
}
}
else
{
startHint = 0;

// We then must check if the object pointed to by the marker is actually
// owned by the account in the request.
auto const sle = ledger->read({ltANY, startAfter});

if (!sle)
return rpcError(rpcINVALID_PARAMS);

if (!RPC::isOwnedByAccount(*ledger, sle, accountID))
return rpcError(rpcINVALID_PARAMS);
}

auto count = 0;
std::optional<uint256> marker = {};
std::uint64_t nextHint = 0;
if (!forEachItemAfter(
*ledger,
accountID,
startAfter,
startHint,
limit - visitData.items.size() + 1,
[&visitData, &accountID](std::shared_ptr<SLE const> const& sleCur) {
if (sleCur && sleCur->getType() == ltPAYCHAN &&
limit + 1,
[&visitData, &accountID, &count, &limit, &marker, &nextHint](
std::shared_ptr<SLE const> const& sleCur) {
if (!sleCur)
{
assert(false);
return false;
}

if (++count == limit)
{
marker = sleCur->key();
nextHint = RPC::getStartHint(sleCur, visitData.accountID);
}

if (count <= limit && sleCur->getType() == ltPAYCHAN &&
(*sleCur)[sfAccount] == accountID &&
(!visitData.hasDst ||
visitData.raDstAccount == (*sleCur)[sfDestination]))
{
visitData.items.emplace_back(sleCur);
return true;
}

return false;
return true;
}))
{
return rpcError(rpcINVALID_PARAMS);
}

if (visitData.items.size() == limit + 1)
// Both conditions need to be checked because marker is set on the limit-th
// item, but if there is no item on the limit + 1 iteration, then there is
// no need to return a marker.
if (count == limit + 1 && marker)
{
result[jss::limit] = limit;

result[jss::marker] = to_string(visitData.items.back()->key());
visitData.items.pop_back();
result[jss::marker] =
to_string(*marker) + "," + std::to_string(nextHint);
}

result[jss::account] = context.app.accountIDCache().toBase58(accountID);
Expand Down
126 changes: 59 additions & 67 deletions src/ripple/rpc/handlers/AccountLines.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ doAccountLines(RPC::JsonContext& context)
if (auto err = readLimitField(limit, RPC::Tuning::accountLines, context))
return *err;

if (limit == 0)
return rpcError(rpcINVALID_PARAMS);

// this flag allows the requester to ask incoming trustlines in default
// state be omitted
bool ignoreDefault = params.isMember(jss::ignore_default) &&
Expand All @@ -138,75 +141,58 @@ doAccountLines(RPC::JsonContext& context)
Json::Value& jsonLines(result[jss::lines] = Json::arrayValue);
VisitData visitData = {
{}, accountID, hasPeer, raPeerAccount, ignoreDefault, 0, nullptr};
unsigned int reserve(limit);
uint256 startAfter;
std::uint64_t startHint;
uint256 startAfter = beast::zero;
std::uint64_t startHint = 0;

if (params.isMember(jss::marker))
{
// We have a start point. Use limit - 1 from the result and use the
// very last one for the resume.
Json::Value const& marker(params[jss::marker]);

if (!marker.isString())
if (!params[jss::marker].isString())
return RPC::expected_field_error(jss::marker, "string");

if (!startAfter.parseHex(marker.asString()))
// Marker is composed of a comma separated index and start hint. The
// former will be read as hex, and the latter using boost lexical cast.
std::stringstream marker(params[jss::marker].asString());
std::string value;
if (!std::getline(marker, value, ','))
return rpcError(rpcINVALID_PARAMS);

auto const sleLine = ledger->read({ltRIPPLE_STATE, startAfter});
if (!startAfter.parseHex(value))
return rpcError(rpcINVALID_PARAMS);

if (!sleLine)
if (!std::getline(marker, value, ','))
return rpcError(rpcINVALID_PARAMS);

bool isDefault = false;
if (sleLine->getFieldAmount(sfLowLimit).getIssuer() == accountID)
try
{
startHint = sleLine->getFieldU64(sfLowNode);
isDefault = !(sleLine->getFieldU32(sfFlags) & lsfLowReserve);
startHint = boost::lexical_cast<std::uint64_t>(value);
}
else if (sleLine->getFieldAmount(sfHighLimit).getIssuer() == accountID)
catch (boost::bad_lexical_cast&)
{
startHint = sleLine->getFieldU64(sfHighNode);
isDefault = !(sleLine->getFieldU32(sfFlags) & lsfHighReserve);
}
else
return rpcError(rpcINVALID_PARAMS);
}

// Caller provided the first line (startAfter), add it as first result
// (but only if it meets inclusion criteria)
// We then must check if the object pointed to by the marker is actually
// owned by the account in the request.
auto const sle = ledger->read({ltANY, startAfter});

if (isDefault && ignoreDefault)
{
// even though we're starting our search here we don't include the
// first entry in this edge case
visitData.items.reserve(++reserve);
}
else
{
auto const line = RippleState::makeItem(accountID, sleLine);
if (line == nullptr)
return rpcError(rpcINVALID_PARAMS);
if (!sle)
return rpcError(rpcINVALID_PARAMS);

addLine(jsonLines, *line);
visitData.items.reserve(reserve);
}
}
else
{
startHint = 0;
// We have no start point, limit should be one higher than requested.
visitData.items.reserve(++reserve);
if (!RPC::isOwnedByAccount(*ledger, sle, accountID))
return rpcError(rpcINVALID_PARAMS);
}

auto count = 0;
std::optional<uint256> marker = {};
std::uint64_t nextHint = 0;
{
if (!forEachItemAfter(
*ledger,
accountID,
startAfter,
startHint,
reserve,
[&visitData](std::shared_ptr<SLE const> const& sleCur) {
limit + 1,
[&visitData, &count, &marker, &limit, &nextHint](std::shared_ptr<SLE const> const& sleCur) {
bool ignore = false;
if (visitData.ignoreDefault)
{
Expand All @@ -219,42 +205,48 @@ doAccountLines(RPC::JsonContext& context)
sleCur->getFieldU32(sfFlags) & lsfHighReserve);
}

auto const line =
RippleState::makeItem(visitData.accountID, sleCur);
if (line != nullptr &&
(!visitData.hasPeer ||
visitData.raPeerAccount == line->getAccountIDPeer()))
if (!sleCur)
{
if (!ignore)
visitData.items.emplace_back(line);
assert(false);
return false;
}

visitData.lastFound = line;
visitData.foundCount++;
if (++count == limit)
{
marker = sleCur->key();
nextHint =
RPC::getStartHint(sleCur, visitData.accountID);
}

return true;
if (!ignore && count <= limit)
{
auto const line =
RippleState::makeItem(visitData.accountID, sleCur);

if (line != nullptr &&
(!visitData.hasPeer ||
visitData.raPeerAccount ==
line->getAccountIDPeer()))
{
visitData.items.emplace_back(line);
}
}

return false;
return true;
}))
{
return rpcError(rpcINVALID_PARAMS);
}
}

// RH Note:
// If ignore_default flag is present all lines must still be iterated, the
// flag only suppresses output. It does not change how iteration works. This
// means the RPC call may return an empty set AND a marker. In this case
// another query must be made until iteration is complete if a complete set
// of non-default state lines are required.
if (visitData.items.size() == reserve || visitData.foundCount >= reserve)
// Both conditions need to be checked because marker is set on the limit-th
// item, but if there is no item on the limit + 1 iteration, then there is
// no need to return a marker.
if (count == limit + 1 && marker)
{
result[jss::limit] = limit;

RippleState::pointer line(visitData.lastFound);
result[jss::marker] = to_string(line->key());
if (visitData.items.back() == visitData.lastFound)
visitData.items.pop_back();
result[jss::marker] =
to_string(*marker) + "," + std::to_string(nextHint);
}

result[jss::account] = context.app.accountIDCache().toBase58(accountID);
Expand Down
Loading

0 comments on commit f0cc98f

Please sign in to comment.