From 0a7ea1bfa0e760919e549befe90dc0d819119d1b Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Wed, 18 Oct 2023 19:16:40 -0400 Subject: [PATCH] feat(rpc): add `server_definitions` method (#4703) Add a new RPC / WS call for `server_definitions`, which returns an SDK-compatible `definitions.json` (binary enum definitions) generated by the server. This enables clients/libraries to dynamically work with new fields and features, such as ones that may become available on side chains. Clients query `server_definitions` on a node from the network they want to work with, and immediately know how to speak that node's binary "language", even if new features are added to it in the future (as long as there are no new serialized types that the software doesn't know how to serialize/deserialize). Example: ```js > {"command": "server_definitions"} < { "result": { "FIELDS": [ [ "Generic", { "isSerialized": false, "isSigningField": false, "isVLEncoded": false, "nth": 0, "type": "Unknown" } ], [ "Invalid", { "isSerialized": false, "isSigningField": false, "isVLEncoded": false, "nth": -1, "type": "Unknown" } ], [ "ObjectEndMarker", { "isSerialized": false, "isSigningField": true, "isVLEncoded": false, "nth": 1, "type": "STObject" } ], ... ``` Close #3657 --------- Co-authored-by: Richard Holland --- API-CHANGELOG.md | 51 +++-- src/ripple/app/main/Main.cpp | 1 + src/ripple/net/impl/RPCCall.cpp | 23 +- src/ripple/protocol/SField.h | 102 +++++---- src/ripple/protocol/TER.h | 6 + src/ripple/protocol/impl/TER.cpp | 11 +- src/ripple/protocol/jss.h | 19 +- src/ripple/rpc/handlers/Handlers.h | 2 + src/ripple/rpc/handlers/ServerInfo.cpp | 291 ++++++++++++++++++++++++- src/ripple/rpc/impl/Handler.cpp | 12 +- src/test/rpc/ServerInfo_test.cpp | 96 ++++++++ 11 files changed, 538 insertions(+), 76 deletions(-) diff --git a/API-CHANGELOG.md b/API-CHANGELOG.md index 5115eee0855..a3c06399cf5 100644 --- a/API-CHANGELOG.md +++ b/API-CHANGELOG.md @@ -26,6 +26,14 @@ In `api_version: 2`, the `signer_lists` field [will be moved](#modifications-to- The `network_id` field was added in the `server_info` response in version 1.5.0 (2019), but it was not returned in [reporting mode](https://xrpl.org/rippled-server-modes.html#reporting-mode). +## Unreleased + +### Additions + +Additions are intended to be non-breaking (because they are purely additive). + +- `server_definitions`: A new RPC that generates a `definitions.json`-like output that can be used in XRPL libraries. + ## XRP Ledger version 1.12.0 [Version 1.12.0](https://github.com/XRPLF/rippled/releases/tag/1.12.0) was released on Sep 6, 2023. @@ -46,27 +54,27 @@ Additions are intended to be non-breaking (because they are purely additive). - `Account`: The issuer of the asset being clawed back. Must also be the sender of the transaction. - `Amount`: The amount being clawed back, with the `Amount.issuer` being the token holder's address. - Adds [AMM](https://github.com/XRPLF/XRPL-Standards/discussions/78) ([#4294](https://github.com/XRPLF/rippled/pull/4294), [#4626](https://github.com/XRPLF/rippled/pull/4626)) feature: - - Adds `amm_info` API to retrieve AMM information for a given tokens pair. - - Adds `AMMCreate` transaction type to create `AMM` instance. - - Adds `AMMDeposit` transaction type to deposit funds into `AMM` instance. - - Adds `AMMWithdraw` transaction type to withdraw funds from `AMM` instance. - - Adds `AMMVote` transaction type to vote for the trading fee of `AMM` instance. - - Adds `AMMBid` transaction type to bid for the Auction Slot of `AMM` instance. - - Adds `AMMDelete` transaction type to delete `AMM` instance. - - Adds `sfAMMID` to `AccountRoot` to indicate that the account is `AMM`'s account. `AMMID` is used to fetch `ltAMM`. - - Adds `lsfAMMNode` `TrustLine` flag to indicate that one side of the `TrustLine` is `AMM` account. - - Adds `tfLPToken`, `tfSingleAsset`, `tfTwoAsset`, `tfOneAssetLPToken`, `tfLimitLPToken`, `tfTwoAssetIfEmpty`, - `tfWithdrawAll`, `tfOneAssetWithdrawAll` which allow a trader to specify different fields combination - for `AMMDeposit` and `AMMWithdraw` transactions. - - Adds new transaction result codes: - - tecUNFUNDED_AMM: insufficient balance to fund AMM. The account does not have funds for liquidity provision. - - tecAMM_BALANCE: AMM has invalid balance. Calculated balances greater than the current pool balances. - - tecAMM_FAILED: AMM transaction failed. Fails due to a processing failure. - - tecAMM_INVALID_TOKENS: AMM invalid LP tokens. Invalid input values, format, or calculated values. - - tecAMM_EMPTY: AMM is in empty state. Transaction expects AMM in non-empty state (LP tokens > 0). - - tecAMM_NOT_EMPTY: AMM is not in empty state. Transaction expects AMM in empty state (LP tokens == 0). - - tecAMM_ACCOUNT: AMM account. Clawback of AMM account. - - tecINCOMPLETE: Some work was completed, but more submissions required to finish. AMMDelete partially deletes the trustlines. + - Adds `amm_info` API to retrieve AMM information for a given tokens pair. + - Adds `AMMCreate` transaction type to create `AMM` instance. + - Adds `AMMDeposit` transaction type to deposit funds into `AMM` instance. + - Adds `AMMWithdraw` transaction type to withdraw funds from `AMM` instance. + - Adds `AMMVote` transaction type to vote for the trading fee of `AMM` instance. + - Adds `AMMBid` transaction type to bid for the Auction Slot of `AMM` instance. + - Adds `AMMDelete` transaction type to delete `AMM` instance. + - Adds `sfAMMID` to `AccountRoot` to indicate that the account is `AMM`'s account. `AMMID` is used to fetch `ltAMM`. + - Adds `lsfAMMNode` `TrustLine` flag to indicate that one side of the `TrustLine` is `AMM` account. + - Adds `tfLPToken`, `tfSingleAsset`, `tfTwoAsset`, `tfOneAssetLPToken`, `tfLimitLPToken`, `tfTwoAssetIfEmpty`, + `tfWithdrawAll`, `tfOneAssetWithdrawAll` which allow a trader to specify different fields combination + for `AMMDeposit` and `AMMWithdraw` transactions. + - Adds new transaction result codes: + - tecUNFUNDED_AMM: insufficient balance to fund AMM. The account does not have funds for liquidity provision. + - tecAMM_BALANCE: AMM has invalid balance. Calculated balances greater than the current pool balances. + - tecAMM_FAILED: AMM transaction failed. Fails due to a processing failure. + - tecAMM_INVALID_TOKENS: AMM invalid LP tokens. Invalid input values, format, or calculated values. + - tecAMM_EMPTY: AMM is in empty state. Transaction expects AMM in non-empty state (LP tokens > 0). + - tecAMM_NOT_EMPTY: AMM is not in empty state. Transaction expects AMM in empty state (LP tokens == 0). + - tecAMM_ACCOUNT: AMM account. Clawback of AMM account. + - tecINCOMPLETE: Some work was completed, but more submissions required to finish. AMMDelete partially deletes the trustlines. ## XRP Ledger version 1.11.0 @@ -118,6 +126,7 @@ Changes below this point are in development. At the time of writing, this version is expected to be introduced in `rippled` version 2.0. Currently (prior to the release of 2.0), it is available as a "beta" version, meaning it can be enabled with a config setting in `rippled.cfg`: + ``` [beta_rpc_api] 1 diff --git a/src/ripple/app/main/Main.cpp b/src/ripple/app/main/Main.cpp index a998640c01c..2ddb771b1e1 100644 --- a/src/ripple/app/main/Main.cpp +++ b/src/ripple/app/main/Main.cpp @@ -169,6 +169,7 @@ printHelp(const po::options_description& desc) " peer_reservations_list\n" " ripple ...\n" " ripple_path_find []\n" + " server_definitions []\n" " server_info [counters]\n" " server_state [counters]\n" " sign [offline]\n" diff --git a/src/ripple/net/impl/RPCCall.cpp b/src/ripple/net/impl/RPCCall.cpp index 1242fdc2a68..b52545960cc 100644 --- a/src/ripple/net/impl/RPCCall.cpp +++ b/src/ripple/net/impl/RPCCall.cpp @@ -1194,6 +1194,20 @@ class RPCParser return jvRequest; } + // server_definitions [hash] + Json::Value + parseServerDefinitions(Json::Value const& jvParams) + { + Json::Value jvRequest{Json::objectValue}; + + if (jvParams.size() == 1) + { + jvRequest[jss::hash] = jvParams[0u].asString(); + } + + return jvRequest; + } + // server_info [counters] Json::Value parseServerInfo(Json::Value const& jvParams) @@ -1255,6 +1269,7 @@ class RPCParser {"channel_verify", &RPCParser::parseChannelVerify, 4, 4}, {"connect", &RPCParser::parseConnect, 1, 2}, {"consensus_info", &RPCParser::parseAsIs, 0, 0}, + {"crawl_shards", &RPCParser::parseAsIs, 0, 2}, {"deposit_authorized", &RPCParser::parseDepositAuthorized, 2, 3}, {"download_shard", &RPCParser::parseDownloadShard, 2, -1}, {"feature", &RPCParser::parseFeature, 0, 2}, @@ -1292,14 +1307,14 @@ class RPCParser 1}, {"peer_reservations_list", &RPCParser::parseAsIs, 0, 0}, {"ripple_path_find", &RPCParser::parseRipplePathFind, 1, 2}, + {"server_definitions", &RPCParser::parseServerDefinitions, 0, 1}, + {"server_info", &RPCParser::parseServerInfo, 0, 1}, + {"server_state", &RPCParser::parseServerInfo, 0, 1}, {"sign", &RPCParser::parseSignSubmit, 2, 3}, {"sign_for", &RPCParser::parseSignFor, 3, 4}, + {"stop", &RPCParser::parseAsIs, 0, 0}, {"submit", &RPCParser::parseSignSubmit, 1, 3}, {"submit_multisigned", &RPCParser::parseSubmitMultiSigned, 1, 1}, - {"server_info", &RPCParser::parseServerInfo, 0, 1}, - {"server_state", &RPCParser::parseServerInfo, 0, 1}, - {"crawl_shards", &RPCParser::parseAsIs, 0, 2}, - {"stop", &RPCParser::parseAsIs, 0, 0}, {"transaction_entry", &RPCParser::parseTransactionEntry, 2, 2}, {"tx", &RPCParser::parseTx, 1, 4}, {"tx_account", &RPCParser::parseTxAccount, 1, 7}, diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index fe045d001d5..5d7acb12383 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -52,43 +52,65 @@ class STInteger; class STXChainBridge; class STVector256; -enum SerializedTypeID { - // special types - STI_UNKNOWN = -2, - STI_NOTPRESENT = 0, - - // // types (common) - STI_UINT16 = 1, - STI_UINT32 = 2, - STI_UINT64 = 3, - STI_UINT128 = 4, - STI_UINT256 = 5, - STI_AMOUNT = 6, - STI_VL = 7, - STI_ACCOUNT = 8, - // 9-13 are reserved - STI_OBJECT = 14, - STI_ARRAY = 15, - - // types (uncommon) - STI_UINT8 = 16, - STI_UINT160 = 17, - STI_PATHSET = 18, - STI_VECTOR256 = 19, - STI_UINT96 = 20, - STI_UINT192 = 21, - STI_UINT384 = 22, - STI_UINT512 = 23, - STI_ISSUE = 24, - STI_XCHAIN_BRIDGE = 25, - - // high level types - // cannot be serialized inside other types - STI_TRANSACTION = 10001, - STI_LEDGERENTRY = 10002, - STI_VALIDATION = 10003, - STI_METADATA = 10004, -}; +#pragma push_macro("XMACRO") +#undef XMACRO + +#define XMACRO(STYPE) \ + /* special types */ \ + STYPE(STI_UNKNOWN, -2) \ + STYPE(STI_NOTPRESENT, 0) \ + STYPE(STI_UINT16, 1) \ + \ + /* types (common) */ \ + STYPE(STI_UINT32, 2) \ + STYPE(STI_UINT64, 3) \ + STYPE(STI_UINT128, 4) \ + STYPE(STI_UINT256, 5) \ + STYPE(STI_AMOUNT, 6) \ + STYPE(STI_VL, 7) \ + STYPE(STI_ACCOUNT, 8) \ + \ + /* 9-13 are reserved */ \ + STYPE(STI_OBJECT, 14) \ + STYPE(STI_ARRAY, 15) \ + \ + /* types (uncommon) */ \ + STYPE(STI_UINT8, 16) \ + STYPE(STI_UINT160, 17) \ + STYPE(STI_PATHSET, 18) \ + STYPE(STI_VECTOR256, 19) \ + STYPE(STI_UINT96, 20) \ + STYPE(STI_UINT192, 21) \ + STYPE(STI_UINT384, 22) \ + STYPE(STI_UINT512, 23) \ + STYPE(STI_ISSUE, 24) \ + STYPE(STI_XCHAIN_BRIDGE, 25) \ + \ + /* high-level types */ \ + /* cannot be serialized inside other types */ \ + STYPE(STI_TRANSACTION, 10001) \ + STYPE(STI_LEDGERENTRY, 10002) \ + STYPE(STI_VALIDATION, 10003) \ + STYPE(STI_METADATA, 10004) + +#pragma push_macro("TO_ENUM") +#undef TO_ENUM +#pragma push_macro("TO_MAP") +#undef TO_MAP + +#define TO_ENUM(name, value) name = value, +#define TO_MAP(name, value) {#name, value}, + +enum SerializedTypeID { XMACRO(TO_ENUM) }; + +static std::map const sTypeMap = {XMACRO(TO_MAP)}; + +#undef XMACRO +#undef TO_ENUM + +#pragma pop_macro("XMACRO") +#pragma pop_macro("TO_ENUM") +#pragma pop_macro("TO_MAP") // constexpr inline int @@ -266,6 +288,12 @@ class SField static int compare(const SField& f1, const SField& f2); + static std::map const& + getKnownCodeToField() + { + return knownCodeToField; + } + private: static int num; static std::map knownCodeToField; diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index f832fe24bce..23d4fb3ef00 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -26,6 +26,7 @@ #include #include #include +#include namespace ripple { @@ -644,6 +645,11 @@ isTecClaim(TER x) return ((x) >= tecCLAIM); } +std::unordered_map< + TERUnderlyingType, + std::pair> const& +transResults(); + bool transResultInfo(TER code, std::string& token, std::string& text); diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index f48bef5232d..1c2db3feb3b 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -20,13 +20,10 @@ #include #include #include -#include namespace ripple { -namespace detail { - -static std::unordered_map< +std::unordered_map< TERUnderlyingType, std::pair> const& transResults() @@ -225,12 +222,10 @@ transResults() return results; } -} // namespace detail - bool transResultInfo(TER code, std::string& token, std::string& text) { - auto& results = detail::transResults(); + auto& results = transResults(); auto const r = results.find(TERtoInt(code)); @@ -264,7 +259,7 @@ std::optional transCode(std::string const& token) { static auto const results = [] { - auto& byTer = detail::transResults(); + auto& byTer = transResults(); auto range = boost::make_iterator_range(byTer.begin(), byTer.end()); auto tRange = boost::adaptors::transform(range, [](auto const& r) { return std::make_pair(r.second.first, r.first); diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index e2800dc80a0..4a8f80b8c7b 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -321,6 +321,8 @@ JSS(fee_level); // out: AccountInfo JSS(fee_mult_max); // in: TransactionSign JSS(fee_ref); // out: NetworkOPs, DEPRECATED JSS(fetch_pack); // out: NetworkOPs +JSS(FIELDS); // out: RPC server_definitions + // matches definitions.json format JSS(first); // out: rpc/Version JSS(firstSequence); // out: NodeToShardStatus JSS(firstShardIndex); // out: NodeToShardStatus @@ -365,6 +367,12 @@ JSS(invalid_API_version); // out: Many, when a request has an invalid JSS(io_latency_ms); // out: NetworkOPs JSS(ip); // in: Connect, out: OverlayImpl JSS(is_burned); // out: nft_info (clio) +JSS(isSerialized); // out: RPC server_definitions + // matches definitions.json format +JSS(isSigningField); // out: RPC server_definitions + // matches definitions.json format +JSS(isVLEncoded); // out: RPC server_definitions + // matches definitions.json format JSS(issuer); // in: RipplePathFind, Subscribe, // Unsubscribe, BookOffers // out: STPathSet, STAmount @@ -404,6 +412,8 @@ JSS(ledger_index_min); // in, out: AccountTx* JSS(ledger_max); // in, out: AccountTx* JSS(ledger_min); // in, out: AccountTx* JSS(ledger_time); // out: NetworkOPs +JSS(LEDGER_ENTRY_TYPES); // out: RPC server_definitions + // matches definitions.json format JSS(levels); // LogLevels JSS(limit); // in/out: AccountTx*, AccountOffers, // AccountLines, AccountObjects @@ -490,6 +500,7 @@ JSS(node_written_bytes); // out: GetCounts JSS(node_writes_duration_us); // out: GetCounts JSS(node_write_retries); // out: GetCounts JSS(node_writes_delayed); // out::GetCounts +JSS(nth); // out: RPC server_definitions JSS(obligations); // out: GatewayBalances JSS(offer); // in: LedgerEntry JSS(offers); // out: NetworkOPs, AccountOffers, Subscribe @@ -649,6 +660,12 @@ JSS(transaction); // in: Tx JSS(transaction_hash); // out: RCLCxPeerPos, LedgerToJson JSS(transactions); // out: LedgerToJson, // in: AccountTx*, Unsubscribe +JSS(TRANSACTION_RESULTS); // out: RPC server_definitions + // matches definitions.json format +JSS(TRANSACTION_TYPES); // out: RPC server_definitions + // matches definitions.json format +JSS(TYPES); // out: RPC server_definitions + // matches definitions.json format JSS(transfer_rate); // out: nft_info (clio) JSS(transitions); // out: NetworkOPs JSS(treenode_cache_size); // out: GetCounts @@ -680,7 +697,7 @@ JSS(txr_not_enabled_cnt); // out: peers with tx reduce-relay disabled count JSS(txr_missing_tx_freq); // out: missing tx frequency average JSS(txs); // out: TxHistory JSS(type); // in: AccountObjects - // out: NetworkOPs + // out: NetworkOPs, RPC server_definitions // OverlayImpl, Logic JSS(type_hex); // out: STPathSet JSS(unl); // out: UnlList diff --git a/src/ripple/rpc/handlers/Handlers.h b/src/ripple/rpc/handlers/Handlers.h index 367e715ce1f..ba93be54513 100644 --- a/src/ripple/rpc/handlers/Handlers.h +++ b/src/ripple/rpc/handlers/Handlers.h @@ -127,6 +127,8 @@ doPeerReservationsList(RPC::JsonContext&); Json::Value doRipplePathFind(RPC::JsonContext&); Json::Value +doServerDefinitions(RPC::JsonContext&); +Json::Value doServerInfo(RPC::JsonContext&); // for humans Json::Value doServerState(RPC::JsonContext&); // for machines diff --git a/src/ripple/rpc/handlers/ServerInfo.cpp b/src/ripple/rpc/handlers/ServerInfo.cpp index aad3d9988e3..2637c3d51be 100644 --- a/src/ripple/rpc/handlers/ServerInfo.cpp +++ b/src/ripple/rpc/handlers/ServerInfo.cpp @@ -22,13 +22,302 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include -#include + +#include + +#include namespace ripple { +namespace detail { + +class ServerDefinitions +{ +private: + std::string + // translate e.g. STI_LEDGERENTRY to LedgerEntry + translate(std::string const& inp); + + uint256 defsHash_; + Json::Value defs_; + +public: + ServerDefinitions(); + + bool + hashMatches(uint256 hash) const + { + return defsHash_ == hash; + } + + Json::Value const& + get() const + { + return defs_; + } +}; + +std::string +ServerDefinitions::translate(std::string const& inp) +{ + auto replace = [&](char const* oldStr, char const* newStr) -> std::string { + std::string out = inp; + boost::replace_all(out, oldStr, newStr); + return out; + }; + + auto contains = [&](char const* s) -> bool { + return inp.find(s) != std::string::npos; + }; + + if (contains("UINT")) + { + if (contains("256") || contains("160") || contains("128")) + return replace("UINT", "Hash"); + else + return replace("UINT", "UInt"); + } + + std::unordered_map replacements{ + {"OBJECT", "STObject"}, + {"ARRAY", "STArray"}, + {"ACCOUNT", "AccountID"}, + {"LEDGERENTRY", "LedgerEntry"}, + {"NOTPRESENT", "NotPresent"}, + {"PATHSET", "PathSet"}, + {"VL", "Blob"}, + {"XCHAIN_BRIDGE", "XChainBridge"}, + }; + + if (auto const& it = replacements.find(inp); it != replacements.end()) + { + return it->second; + } + + std::string out; + size_t pos = 0; + std::string inpToProcess = inp; + + // convert snake_case to CamelCase + for (;;) + { + pos = inpToProcess.find("_"); + if (pos == std::string::npos) + pos = inpToProcess.size(); + std::string token = inpToProcess.substr(0, pos); + if (token.size() > 1) + { + boost::algorithm::to_lower(token); + token.data()[0] -= ('a' - 'A'); + out += token; + } + else + out += token; + if (pos == inpToProcess.size()) + break; + inpToProcess = inpToProcess.substr(pos + 1); + } + return out; +}; + +ServerDefinitions::ServerDefinitions() : defs_{Json::objectValue} +{ + // populate SerializedTypeID names and values + defs_[jss::TYPES] = Json::objectValue; + + defs_[jss::TYPES]["Done"] = -1; + std::map typeMap{{-1, "Done"}}; + for (auto const& [rawName, typeValue] : sTypeMap) + { + std::string typeName = + translate(std::string(rawName).substr(4) /* remove STI_ */); + defs_[jss::TYPES][typeName] = typeValue; + typeMap[typeValue] = typeName; + } + + // populate LedgerEntryType names and values + defs_[jss::LEDGER_ENTRY_TYPES] = Json::objectValue; + defs_[jss::LEDGER_ENTRY_TYPES][jss::Invalid] = -1; + + for (auto const& f : LedgerFormats::getInstance()) + { + defs_[jss::LEDGER_ENTRY_TYPES][f.getName()] = f.getType(); + } + + // populate SField serialization data + defs_[jss::FIELDS] = Json::arrayValue; + + uint32_t i = 0; + { + Json::Value a = Json::arrayValue; + a[0U] = "Generic"; + Json::Value v = Json::objectValue; + v[jss::nth] = 0; + v[jss::isVLEncoded] = false; + v[jss::isSerialized] = false; + v[jss::isSigningField] = false; + v[jss::type] = "Unknown"; + a[1U] = v; + defs_[jss::FIELDS][i++] = a; + } + + { + Json::Value a = Json::arrayValue; + a[0U] = "Invalid"; + Json::Value v = Json::objectValue; + v[jss::nth] = -1; + v[jss::isVLEncoded] = false; + v[jss::isSerialized] = false; + v[jss::isSigningField] = false; + v[jss::type] = "Unknown"; + a[1U] = v; + defs_[jss::FIELDS][i++] = a; + } + + { + Json::Value a = Json::arrayValue; + a[0U] = "ObjectEndMarker"; + Json::Value v = Json::objectValue; + v[jss::nth] = 1; + v[jss::isVLEncoded] = false; + v[jss::isSerialized] = true; + v[jss::isSigningField] = true; + v[jss::type] = "STObject"; + a[1U] = v; + defs_[jss::FIELDS][i++] = a; + } + + { + Json::Value a = Json::arrayValue; + a[0U] = "ArrayEndMarker"; + Json::Value v = Json::objectValue; + v[jss::nth] = 1; + v[jss::isVLEncoded] = false; + v[jss::isSerialized] = true; + v[jss::isSigningField] = true; + v[jss::type] = "STArray"; + a[1U] = v; + defs_[jss::FIELDS][i++] = a; + } + + { + Json::Value a = Json::arrayValue; + a[0U] = "taker_gets_funded"; + Json::Value v = Json::objectValue; + v[jss::nth] = 258; + v[jss::isVLEncoded] = false; + v[jss::isSerialized] = false; + v[jss::isSigningField] = false; + v[jss::type] = "Amount"; + a[1U] = v; + defs_[jss::FIELDS][i++] = a; + } + + { + Json::Value a = Json::arrayValue; + a[0U] = "taker_pays_funded"; + Json::Value v = Json::objectValue; + v[jss::nth] = 259; + v[jss::isVLEncoded] = false; + v[jss::isSerialized] = false; + v[jss::isSigningField] = false; + v[jss::type] = "Amount"; + a[1U] = v; + defs_[jss::FIELDS][i++] = a; + } + + for (auto const& [code, f] : ripple::SField::getKnownCodeToField()) + { + if (f->fieldName == "") + continue; + + Json::Value innerObj = Json::objectValue; + + uint32_t type = f->fieldType; + + innerObj[jss::nth] = f->fieldValue; + + // whether the field is variable-length encoded + // this means that the length is included before the content + innerObj[jss::isVLEncoded] = + (type == 7U /* Blob */ || type == 8U /* AccountID */ || + type == 19U /* Vector256 */); + + // whether the field is included in serialization + innerObj[jss::isSerialized] = + (type < 10000 && f->fieldName != "hash" && + f->fieldName != "index"); /* hash, index, TRANSACTION, + LEDGER_ENTRY, VALIDATION, METADATA */ + + // whether the field is included in serialization when signing + innerObj[jss::isSigningField] = f->shouldInclude(false); + + innerObj[jss::type] = typeMap[type]; + + Json::Value innerArray = Json::arrayValue; + innerArray[0U] = f->fieldName; + innerArray[1U] = innerObj; + + defs_[jss::FIELDS][i++] = innerArray; + } + + // populate TER code names and values + defs_[jss::TRANSACTION_RESULTS] = Json::objectValue; + + for (auto const& [code, terInfo] : transResults()) + { + defs_[jss::TRANSACTION_RESULTS][terInfo.first] = code; + } + + // populate TxType names and values + defs_[jss::TRANSACTION_TYPES] = Json::objectValue; + defs_[jss::TRANSACTION_TYPES][jss::Invalid] = -1; + for (auto const& f : TxFormats::getInstance()) + { + defs_[jss::TRANSACTION_TYPES][f.getName()] = f.getType(); + } + + // generate hash + { + const std::string out = Json::FastWriter().write(defs_); + defsHash_ = ripple::sha512Half(ripple::Slice{out.data(), out.size()}); + defs_[jss::hash] = to_string(defsHash_); + } +} + +} // namespace detail + +Json::Value +doServerDefinitions(RPC::JsonContext& context) +{ + auto& params = context.params; + + uint256 hash; + if (params.isMember(jss::hash)) + { + if (!params[jss::hash].isString() || + !hash.parseHex(params[jss::hash].asString())) + return RPC::invalid_field_error(jss::hash); + } + + static const detail::ServerDefinitions defs{}; + if (defs.hashMatches(hash)) + { + Json::Value jv = Json::objectValue; + jv[jss::hash] = to_string(hash); + return jv; + } + return defs.get(); +} + Json::Value doServerInfo(RPC::JsonContext& context) { diff --git a/src/ripple/rpc/impl/Handler.cpp b/src/ripple/rpc/impl/Handler.cpp index dd898ee8722..b69d2608b0e 100644 --- a/src/ripple/rpc/impl/Handler.cpp +++ b/src/ripple/rpc/impl/Handler.cpp @@ -80,6 +80,7 @@ Handler const handlerArray[]{ {"channel_verify", byRef(&doChannelVerify), Role::USER, NO_CONDITION}, {"connect", byRef(&doConnect), Role::ADMIN, NO_CONDITION}, {"consensus_info", byRef(&doConsensusInfo), Role::ADMIN, NO_CONDITION}, + {"crawl_shards", byRef(&doCrawlShards), Role::ADMIN, NO_CONDITION}, {"deposit_authorized", byRef(&doDepositAuthorized), Role::USER, @@ -139,17 +140,20 @@ Handler const handlerArray[]{ Role::ADMIN, NO_CONDITION}, {"ripple_path_find", byRef(&doRipplePathFind), Role::USER, NO_CONDITION}, + {"server_definitions", + byRef(&doServerDefinitions), + Role::USER, + NO_CONDITION}, + {"server_info", byRef(&doServerInfo), Role::USER, NO_CONDITION}, + {"server_state", byRef(&doServerState), Role::USER, NO_CONDITION}, {"sign", byRef(&doSign), Role::USER, NO_CONDITION}, {"sign_for", byRef(&doSignFor), Role::USER, NO_CONDITION}, + {"stop", byRef(&doStop), Role::ADMIN, NO_CONDITION}, {"submit", byRef(&doSubmit), Role::USER, NEEDS_CURRENT_LEDGER}, {"submit_multisigned", byRef(&doSubmitMultiSigned), Role::USER, NEEDS_CURRENT_LEDGER}, - {"server_info", byRef(&doServerInfo), Role::USER, NO_CONDITION}, - {"server_state", byRef(&doServerState), Role::USER, NO_CONDITION}, - {"crawl_shards", byRef(&doCrawlShards), Role::ADMIN, NO_CONDITION}, - {"stop", byRef(&doStop), Role::ADMIN, NO_CONDITION}, {"transaction_entry", byRef(&doTransactionEntry), Role::USER, NO_CONDITION}, {"tx", byRef(&doTxJson), Role::USER, NEEDS_NETWORK_CONNECTION}, {"tx_history", byRef(&doTxHistory), Role::USER, NO_CONDITION}, diff --git a/src/test/rpc/ServerInfo_test.cpp b/src/test/rpc/ServerInfo_test.cpp index a69483cb130..1d78f1cc36b 100644 --- a/src/test/rpc/ServerInfo_test.cpp +++ b/src/test/rpc/ServerInfo_test.cpp @@ -79,6 +79,8 @@ admin = 127.0.0.1 void testServerInfo() { + testcase("server_info"); + using namespace test::jtx; { @@ -148,10 +150,104 @@ admin = 127.0.0.1 } } + void + testServerDefinitions() + { + testcase("server_definitions"); + + using namespace test::jtx; + + { + Env env(*this); + auto const result = env.rpc("server_definitions"); + BEAST_EXPECT(!result[jss::result].isMember(jss::error)); + BEAST_EXPECT(result[jss::result][jss::status] == "success"); + BEAST_EXPECT(result[jss::result].isMember(jss::FIELDS)); + BEAST_EXPECT(result[jss::result].isMember(jss::LEDGER_ENTRY_TYPES)); + BEAST_EXPECT( + result[jss::result].isMember(jss::TRANSACTION_RESULTS)); + BEAST_EXPECT(result[jss::result].isMember(jss::TRANSACTION_TYPES)); + BEAST_EXPECT(result[jss::result].isMember(jss::TYPES)); + BEAST_EXPECT(result[jss::result].isMember(jss::hash)); + + // test a random element of each result + // (testing the whole output would be difficult to maintain) + + { + auto const firstField = result[jss::result][jss::FIELDS][0u]; + BEAST_EXPECT(firstField[0u].asString() == "Generic"); + BEAST_EXPECT( + firstField[1][jss::isSerialized].asBool() == false); + BEAST_EXPECT( + firstField[1][jss::isSigningField].asBool() == false); + BEAST_EXPECT(firstField[1][jss::isVLEncoded].asBool() == false); + BEAST_EXPECT(firstField[1][jss::nth].asUInt() == 0); + BEAST_EXPECT(firstField[1][jss::type].asString() == "Unknown"); + } + + BEAST_EXPECT( + result[jss::result][jss::LEDGER_ENTRY_TYPES]["AccountRoot"] + .asUInt() == 97); + BEAST_EXPECT( + result[jss::result][jss::TRANSACTION_RESULTS]["tecDIR_FULL"] + .asUInt() == 121); + BEAST_EXPECT( + result[jss::result][jss::TRANSACTION_TYPES]["Payment"] + .asUInt() == 0); + BEAST_EXPECT( + result[jss::result][jss::TYPES]["AccountID"].asUInt() == 8); + } + + // test providing the same hash + { + Env env(*this); + auto const firstResult = env.rpc("server_definitions"); + auto const hash = firstResult[jss::result][jss::hash].asString(); + auto const hashParam = + std::string("{ ") + "\"hash\": \"" + hash + "\"}"; + + auto const result = + env.rpc("json", "server_definitions", hashParam); + BEAST_EXPECT(!result[jss::result].isMember(jss::error)); + BEAST_EXPECT(result[jss::result][jss::status] == "success"); + BEAST_EXPECT(!result[jss::result].isMember(jss::FIELDS)); + BEAST_EXPECT( + !result[jss::result].isMember(jss::LEDGER_ENTRY_TYPES)); + BEAST_EXPECT( + !result[jss::result].isMember(jss::TRANSACTION_RESULTS)); + BEAST_EXPECT(!result[jss::result].isMember(jss::TRANSACTION_TYPES)); + BEAST_EXPECT(!result[jss::result].isMember(jss::TYPES)); + BEAST_EXPECT(result[jss::result].isMember(jss::hash)); + } + + // test providing a different hash + { + Env env(*this); + std::string const hash = + "54296160385A27154BFA70A239DD8E8FD4CC2DB7BA32D970BA3A5B132CF749" + "D1"; + auto const hashParam = + std::string("{ ") + "\"hash\": \"" + hash + "\"}"; + + auto const result = + env.rpc("json", "server_definitions", hashParam); + BEAST_EXPECT(!result[jss::result].isMember(jss::error)); + BEAST_EXPECT(result[jss::result][jss::status] == "success"); + BEAST_EXPECT(result[jss::result].isMember(jss::FIELDS)); + BEAST_EXPECT(result[jss::result].isMember(jss::LEDGER_ENTRY_TYPES)); + BEAST_EXPECT( + result[jss::result].isMember(jss::TRANSACTION_RESULTS)); + BEAST_EXPECT(result[jss::result].isMember(jss::TRANSACTION_TYPES)); + BEAST_EXPECT(result[jss::result].isMember(jss::TYPES)); + BEAST_EXPECT(result[jss::result].isMember(jss::hash)); + } + } + void run() override { testServerInfo(); + testServerDefinitions(); } };