Skip to content

Commit 90db6bc

Browse files
committed
feat: add user version of feature RPC (#4781)
* uses same formatting as admin RPC * hides potentially sensitive data
1 parent 97863e0 commit 90db6bc

File tree

7 files changed

+140
-23
lines changed

7 files changed

+140
-23
lines changed

API-CHANGELOG.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ The `network_id` field was added in the `server_info` response in version 1.5.0
2828

2929
## XRP Ledger server version 2.0.0
3030

31+
### Additions in 2.2
32+
33+
Additions are intended to be non-breaking (because they are purely additive).
34+
35+
- `feature`: A non-admin mode that uses the same formatting as admin RPC, but hides potentially-sensitive data.
36+
3137
### Additions in 2.0
3238

3339
Additions are intended to be non-breaking (because they are purely additive).
@@ -36,7 +42,6 @@ Additions are intended to be non-breaking (because they are purely additive).
3642
- In `Payment` transactions, `DeliverMax` has been added. This is a replacement for the `Amount` field, which should not be used. Typically, the `delivered_amount` (in transaction metadata) should be used. To ease the transition, `DeliverMax` is present regardless of API version, since adding a field is non-breaking.
3743
- API version 2 has been moved from beta to supported, meaning that it is generally available (regardless of the `beta_rpc_api` setting).
3844

39-
4045
## XRP Ledger server version 1.12.0
4146

4247
[Version 1.12.0](https://github.com/XRPLF/rippled/releases/tag/1.12.0) was released on Sep 6, 2023.

src/ripple/app/misc/AmendmentTable.h

+2-2
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,11 @@ class AmendmentTable
8181
firstUnsupportedExpected() const = 0;
8282

8383
virtual Json::Value
84-
getJson() const = 0;
84+
getJson(bool isAdmin) const = 0;
8585

8686
/** Returns a Json::objectValue. */
8787
virtual Json::Value
88-
getJson(uint256 const& amendment) const = 0;
88+
getJson(uint256 const& amendment, bool isAdmin) const = 0;
8989

9090
/** Called when a new fully-validated ledger is accepted. */
9191
void

src/ripple/app/misc/impl/AmendmentTable.cpp

+14-8
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ class AmendmentTableImpl final : public AmendmentTable
388388
Json::Value& v,
389389
uint256 const& amendment,
390390
AmendmentState const& state,
391+
bool isAdmin,
391392
std::lock_guard<std::mutex> const& lock) const;
392393

393394
void
@@ -428,9 +429,9 @@ class AmendmentTableImpl final : public AmendmentTable
428429
firstUnsupportedExpected() const override;
429430

430431
Json::Value
431-
getJson() const override;
432+
getJson(bool isAdmin) const override;
432433
Json::Value
433-
getJson(uint256 const&) const override;
434+
getJson(uint256 const&, bool isAdmin) const override;
434435

435436
bool
436437
needValidatedLedger(LedgerIndex seq) const override;
@@ -906,13 +907,14 @@ AmendmentTableImpl::injectJson(
906907
Json::Value& v,
907908
const uint256& id,
908909
const AmendmentState& fs,
910+
bool isAdmin,
909911
std::lock_guard<std::mutex> const&) const
910912
{
911913
if (!fs.name.empty())
912914
v[jss::name] = fs.name;
913915

914916
v[jss::supported] = fs.supported;
915-
if (!fs.enabled)
917+
if (!fs.enabled && isAdmin)
916918
{
917919
if (fs.vote == AmendmentVote::obsolete)
918920
v[jss::vetoed] = "Obsolete";
@@ -921,7 +923,7 @@ AmendmentTableImpl::injectJson(
921923
}
922924
v[jss::enabled] = fs.enabled;
923925

924-
if (!fs.enabled && lastVote_)
926+
if (!fs.enabled && lastVote_ && isAdmin)
925927
{
926928
auto const votesTotal = lastVote_->trustedValidations();
927929
auto const votesNeeded = lastVote_->threshold();
@@ -936,7 +938,7 @@ AmendmentTableImpl::injectJson(
936938
}
937939

938940
Json::Value
939-
AmendmentTableImpl::getJson() const
941+
AmendmentTableImpl::getJson(bool isAdmin) const
940942
{
941943
Json::Value ret(Json::objectValue);
942944
{
@@ -947,23 +949,27 @@ AmendmentTableImpl::getJson() const
947949
ret[to_string(e.first)] = Json::objectValue,
948950
e.first,
949951
e.second,
952+
isAdmin,
950953
lock);
951954
}
952955
}
953956
return ret;
954957
}
955958

956959
Json::Value
957-
AmendmentTableImpl::getJson(uint256 const& amendmentID) const
960+
AmendmentTableImpl::getJson(uint256 const& amendmentID, bool isAdmin) const
958961
{
959962
Json::Value ret = Json::objectValue;
960-
Json::Value& jAmendment = (ret[to_string(amendmentID)] = Json::objectValue);
961963

962964
{
963965
std::lock_guard lock(mutex_);
964966
AmendmentState const* a = get(amendmentID, lock);
965967
if (a)
966-
injectJson(jAmendment, amendmentID, *a, lock);
968+
{
969+
Json::Value& jAmendment =
970+
(ret[to_string(amendmentID)] = Json::objectValue);
971+
injectJson(jAmendment, amendmentID, *a, isAdmin, lock);
972+
}
967973
}
968974

969975
return ret;

src/ripple/rpc/handlers/Feature1.cpp

+9-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include <ripple/app/misc/AmendmentTable.h>
2323
#include <ripple/net/RPCErr.h>
2424
#include <ripple/protocol/ErrorCodes.h>
25+
#include <ripple/protocol/Feature.h>
2526
#include <ripple/protocol/jss.h>
2627
#include <ripple/rpc/Context.h>
2728

@@ -37,6 +38,7 @@ doFeature(RPC::JsonContext& context)
3738
if (context.app.config().reporting())
3839
return rpcError(rpcREPORTING_UNSUPPORTED);
3940

41+
bool const isAdmin = context.role == Role::ADMIN;
4042
// Get majority amendment status
4143
majorityAmendments_t majorities;
4244

@@ -47,7 +49,7 @@ doFeature(RPC::JsonContext& context)
4749

4850
if (!context.params.isMember(jss::feature))
4951
{
50-
auto features = table.getJson();
52+
auto features = table.getJson(isAdmin);
5153

5254
for (auto const& [h, t] : majorities)
5355
{
@@ -69,13 +71,18 @@ doFeature(RPC::JsonContext& context)
6971

7072
if (context.params.isMember(jss::vetoed))
7173
{
74+
if (!isAdmin)
75+
return rpcError(rpcNO_PERMISSION);
76+
7277
if (context.params[jss::vetoed].asBool())
7378
table.veto(feature);
7479
else
7580
table.unVeto(feature);
7681
}
7782

78-
Json::Value jvReply = table.getJson(feature);
83+
Json::Value jvReply = table.getJson(feature, isAdmin);
84+
if (!jvReply)
85+
return rpcError(rpcBAD_FEATURE);
7986

8087
auto m = majorities.find(feature);
8188
if (m != majorities.end())

src/ripple/rpc/impl/Handler.cpp

+3-3
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ Handler const handlerArray[]{
105105
Role::USER,
106106
NO_CONDITION},
107107
{"download_shard", byRef(&doDownloadShard), Role::ADMIN, NO_CONDITION},
108+
{"feature", byRef(&doFeature), Role::USER, NO_CONDITION},
109+
{"fee", byRef(&doFee), Role::USER, NEEDS_CURRENT_LEDGER},
110+
{"fetch_info", byRef(&doFetchInfo), Role::ADMIN, NO_CONDITION},
108111
#ifdef RIPPLED_REPORTING
109112
{"gateway_balances", byRef(&doGatewayBalances), Role::ADMIN, NO_CONDITION},
110113
#else
@@ -115,9 +118,6 @@ Handler const handlerArray[]{
115118
byRef(&doGetAggregatePrice),
116119
Role::USER,
117120
NO_CONDITION},
118-
{"feature", byRef(&doFeature), Role::ADMIN, NO_CONDITION},
119-
{"fee", byRef(&doFee), Role::USER, NEEDS_CURRENT_LEDGER},
120-
{"fetch_info", byRef(&doFetchInfo), Role::ADMIN, NO_CONDITION},
121121
{"ledger_accept",
122122
byRef(&doLedgerAccept),
123123
Role::ADMIN,

src/test/app/AmendmentTable_test.cpp

+18-2
Original file line numberDiff line numberDiff line change
@@ -288,15 +288,15 @@ class AmendmentTable_test final : public beast::unit_test::suite
288288
uint256 const unsupportedID = amendmentId(unsupported_[0]);
289289
{
290290
Json::Value const unsupp =
291-
table->getJson(unsupportedID)[to_string(unsupportedID)];
291+
table->getJson(unsupportedID, true)[to_string(unsupportedID)];
292292
BEAST_EXPECT(unsupp.size() == 0);
293293
}
294294

295295
// After vetoing unsupportedID verify that it is in table.
296296
table->veto(unsupportedID);
297297
{
298298
Json::Value const unsupp =
299-
table->getJson(unsupportedID)[to_string(unsupportedID)];
299+
table->getJson(unsupportedID, true)[to_string(unsupportedID)];
300300
BEAST_EXPECT(unsupp[jss::vetoed].asBool());
301301
}
302302
}
@@ -638,6 +638,22 @@ class AmendmentTable_test final : public beast::unit_test::suite
638638
BEAST_EXPECT(enabled.empty());
639639
BEAST_EXPECT(majority.empty());
640640

641+
uint256 const unsupportedID = amendmentId(unsupported_[0]);
642+
{
643+
Json::Value const unsupp =
644+
table->getJson(unsupportedID, false)[to_string(unsupportedID)];
645+
BEAST_EXPECT(unsupp.size() == 0);
646+
}
647+
648+
table->veto(unsupportedID);
649+
{
650+
Json::Value const unsupp =
651+
table->getJson(unsupportedID, false)[to_string(unsupportedID)];
652+
BEAST_EXPECT(!unsupp[jss::vetoed].asBool());
653+
}
654+
655+
votes.emplace_back(testAmendment, validators.size());
656+
641657
votes.emplace_back(testAmendment, validators.size());
642658

643659
doRound(

src/test/rpc/Feature_test.cpp

+88-5
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,94 @@ class Feature_test : public beast::unit_test::suite
205205
return cfg;
206206
})};
207207

208-
auto jrr = env.rpc("feature")[jss::result];
209-
// The current HTTP/S ServerHandler returns an HTTP 403 error code here
210-
// rather than a noPermission JSON error. The JSONRPCClient just eats
211-
// that error and returns an null result.
212-
BEAST_EXPECT(jrr.isNull());
208+
{
209+
auto result = env.rpc("feature")[jss::result];
210+
BEAST_EXPECT(result.isMember(jss::features));
211+
// There should be at least 50 amendments. Don't do exact
212+
// comparison to avoid maintenance as more amendments are added in
213+
// the future.
214+
BEAST_EXPECT(result[jss::features].size() >= 50);
215+
for (auto it = result[jss::features].begin();
216+
it != result[jss::features].end();
217+
++it)
218+
{
219+
uint256 id;
220+
(void)id.parseHex(it.key().asString().c_str());
221+
if (!BEAST_EXPECT((*it).isMember(jss::name)))
222+
return;
223+
bool expectEnabled =
224+
env.app().getAmendmentTable().isEnabled(id);
225+
bool expectSupported =
226+
env.app().getAmendmentTable().isSupported(id);
227+
BEAST_EXPECTS(
228+
(*it).isMember(jss::enabled) &&
229+
(*it)[jss::enabled].asBool() == expectEnabled,
230+
(*it)[jss::name].asString() + " enabled");
231+
BEAST_EXPECTS(
232+
(*it).isMember(jss::supported) &&
233+
(*it)[jss::supported].asBool() == expectSupported,
234+
(*it)[jss::name].asString() + " supported");
235+
BEAST_EXPECT(!(*it).isMember(jss::vetoed));
236+
BEAST_EXPECT(!(*it).isMember(jss::majority));
237+
BEAST_EXPECT(!(*it).isMember(jss::count));
238+
BEAST_EXPECT(!(*it).isMember(jss::validations));
239+
BEAST_EXPECT(!(*it).isMember(jss::threshold));
240+
}
241+
}
242+
243+
{
244+
Json::Value params;
245+
// invalid feature
246+
params[jss::feature] =
247+
"1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCD"
248+
"EF";
249+
auto const result = env.rpc(
250+
"json",
251+
"feature",
252+
boost::lexical_cast<std::string>(params))[jss::result];
253+
BEAST_EXPECTS(
254+
result[jss::error] == "badFeature", result.toStyledString());
255+
BEAST_EXPECT(
256+
result[jss::error_message] == "Feature unknown or invalid.");
257+
}
258+
259+
{
260+
Json::Value params;
261+
params[jss::feature] =
262+
"93E516234E35E08CA689FA33A6D38E103881F8DCB53023F728C307AA89D515"
263+
"A7";
264+
// invalid param
265+
params[jss::vetoed] = true;
266+
auto const result = env.rpc(
267+
"json",
268+
"feature",
269+
boost::lexical_cast<std::string>(params))[jss::result];
270+
BEAST_EXPECTS(
271+
result[jss::error] == "noPermission",
272+
result[jss::error].asString());
273+
BEAST_EXPECT(
274+
result[jss::error_message] ==
275+
"You don't have permission for this command.");
276+
}
277+
278+
{
279+
std::string const feature =
280+
"C4483A1896170C66C098DEA5B0E024309C60DC960DE5F01CD7AF986AA3D9AD"
281+
"37";
282+
Json::Value params;
283+
params[jss::feature] = feature;
284+
auto const result = env.rpc(
285+
"json",
286+
"feature",
287+
boost::lexical_cast<std::string>(params))[jss::result];
288+
BEAST_EXPECT(result.isMember(feature));
289+
auto const amendmentResult = result[feature];
290+
BEAST_EXPECT(amendmentResult[jss::enabled].asBool() == false);
291+
BEAST_EXPECT(amendmentResult[jss::supported].asBool() == true);
292+
BEAST_EXPECT(
293+
amendmentResult[jss::name].asString() ==
294+
"fixMasterKeyAsRegularKey");
295+
}
213296
}
214297

215298
void

0 commit comments

Comments
 (0)