xrpld
Loading...
Searching...
No Matches
Subscribe.cpp
1#include <xrpld/app/ledger/LedgerMaster.h>
2#include <xrpld/app/main/Application.h>
3#include <xrpld/rpc/Context.h>
4#include <xrpld/rpc/RPCSub.h>
5#include <xrpld/rpc/Role.h>
6#include <xrpld/rpc/detail/RPCHelpers.h>
7#include <xrpld/rpc/detail/Tuning.h>
8
9#include <xrpl/basics/Log.h>
10#include <xrpl/basics/UnorderedContainers.h>
11#include <xrpl/basics/base_uint.h>
12#include <xrpl/json/json_value.h>
13#include <xrpl/ledger/ReadView.h>
14#include <xrpl/protocol/AccountID.h>
15#include <xrpl/protocol/Book.h>
16#include <xrpl/protocol/ErrorCodes.h>
17#include <xrpl/protocol/RPCErr.h>
18#include <xrpl/protocol/jss.h>
19#include <xrpl/resource/Fees.h>
20#include <xrpl/server/InfoSub.h>
21#include <xrpl/server/NetworkOPs.h>
22
23#include <cstddef>
24#include <memory>
25#include <optional>
26#include <stdexcept>
27#include <string>
28
29namespace xrpl {
30
31namespace {
32
41[[nodiscard]] bool
42wouldExceedSubscriptionCap(InfoSub::ref ispSub, std::size_t additional, std::size_t cap)
43{
44 return exceedsSubscriptionCap(ispSub->totalSubscriptionCount(), additional, cap);
45}
46
47} // namespace
48
49json::Value
51{
52 InfoSub::pointer ispSub;
54
55 if (!context.infoSub && !context.params.isMember(jss::url))
56 {
57 // Must be a JSON-RPC call.
58 JLOG(context.j.info()) << "doSubscribe: RPC subscribe requires a url";
60 }
61
62 if (context.params.isMember(jss::url))
63 {
64 if (context.role != Role::ADMIN)
66
67 std::string const strUrl = context.params[jss::url].asString();
68 std::string strUsername = context.params.isMember(jss::url_username)
69 ? context.params[jss::url_username].asString()
70 : "";
71 std::string strPassword = context.params.isMember(jss::url_password)
72 ? context.params[jss::url_password].asString()
73 : "";
74
75 // DEPRECATED
76 if (context.params.isMember(jss::username))
77 strUsername = context.params[jss::username].asString();
78
79 // DEPRECATED
80 if (context.params.isMember(jss::password))
81 strPassword = context.params[jss::password].asString();
82
83 ispSub = context.netOps.findRpcSub(strUrl);
84 if (!ispSub)
85 {
86 JLOG(context.j.debug()) << "doSubscribe: building: " << strUrl;
87 try
88 {
89 auto rspSub = makeRPCSub(
90 context.app.getOPs(),
91 context.app.getIOContext(),
92 context.app.getJobQueue(),
93 strUrl,
94 strUsername,
95 strPassword,
96 context.app);
97 ispSub =
99 }
100 catch (std::runtime_error const& ex)
101 {
102 return rpc::makeParamError(ex.what());
103 }
104 }
105 else
106 {
107 JLOG(context.j.trace()) << "doSubscribe: reusing: " << strUrl;
108
109 if (auto rpcSub = std::dynamic_pointer_cast<RPCSub>(ispSub))
110 {
111 // Why do we need to check isMember against jss::username and
112 // jss::password here instead of just setting the username and
113 // the password? What about url_username and url_password?
114 if (context.params.isMember(jss::username))
115 rpcSub->setUsername(strUsername);
116
117 if (context.params.isMember(jss::password))
118 rpcSub->setPassword(strPassword);
119 }
120 }
121 }
122 else
123 {
124 ispSub = context.infoSub;
125 }
126 ispSub->setApiVersion(context.apiVersion);
127
128 // Effective per-connection subscription cap: a configured override if set,
129 // otherwise the built-in default. Resolved once and reused by every branch.
130 std::size_t const subscriptionCap =
132
133 if (context.params.isMember(jss::streams))
134 {
135 if (!context.params[jss::streams].isArray())
136 {
137 JLOG(context.j.info()) << "doSubscribe: streams requires an array.";
139 }
140
141 for (auto const& it : context.params[jss::streams])
142 {
143 if (!it.isString())
145
146 std::string const streamName = it.asString();
147 if (streamName == "server")
148 {
149 context.netOps.subServer(ispSub, jvResult, context.role == Role::ADMIN);
150 }
151 else if (streamName == "ledger")
152 {
153 context.netOps.subLedger(ispSub, jvResult);
154 }
155 else if (streamName == "book_changes")
156 {
157 context.netOps.subBookChanges(ispSub);
158 }
159 else if (streamName == "manifests")
160 {
161 context.netOps.subManifests(ispSub);
162 }
163 else if (streamName == "transactions")
164 {
165 context.netOps.subTransactions(ispSub);
166 }
167 else if (
168 streamName == "transactions_proposed" ||
169 streamName == "rt_transactions") // DEPRECATED
170 {
171 context.netOps.subRTTransactions(ispSub);
172 }
173 else if (streamName == "validations")
174 {
175 context.netOps.subValidations(ispSub);
176 }
177 else if (streamName == "peer_status")
178 {
179 if (context.role != Role::ADMIN)
181 context.netOps.subPeerStatus(ispSub);
182 }
183 else if (streamName == "consensus")
184 {
185 context.netOps.subConsensus(ispSub);
186 }
187 else
188 {
190 }
191 }
192 }
193
194 // Parse the proposed (real-time) and normal account sets first, then check
195 // the cap against their COMBINED net-new total before subscribing either.
196 // This keeps the account pair all-or-nothing: it never subscribes one set
197 // and then rejects on the other. Other fields (streams and account_history)
198 // are still checked and subscribed independently, as they always have been,
199 // so a later field can be rejected after an earlier one subscribed. The cap
200 // counts only NET-NEW accounts (those not already tracked on this
201 // connection), so re-subscribing accounts already held is never wrongly
202 // rejected.
203 auto accountsProposed = context.params.isMember(jss::accounts_proposed)
204 ? jss::accounts_proposed
205 : jss::rt_accounts; // DEPRECATED
206 bool const hasProposed = context.params.isMember(accountsProposed);
207 bool const hasAccounts = context.params.isMember(jss::accounts);
208
209 hash_set<AccountID> proposedIds;
210 hash_set<AccountID> accountIds;
211
212 if (hasProposed)
213 {
214 if (!context.params[accountsProposed].isArray())
216
217 proposedIds = rpc::parseAccountIds(context.params[accountsProposed]);
218 if (proposedIds.empty())
220 }
221
222 if (hasAccounts)
223 {
224 if (!context.params[jss::accounts].isArray())
226
227 accountIds = rpc::parseAccountIds(context.params[jss::accounts]);
228 if (accountIds.empty())
230 }
231
232 if (hasProposed || hasAccounts)
233 {
234 // Atomic check-and-reserve, so two concurrent requests sharing this
235 // InfoSub (admin subscribe-by-url) cannot both pass the cap check.
236 if (!ispSub->tryReserveAccountSubscriptions(proposedIds, accountIds, subscriptionCap))
237 return rpc::makeParamError("Too many subscriptions for this connection.");
238 }
239
240 if (hasProposed)
241 context.netOps.subAccount(ispSub, proposedIds, true);
242
243 if (hasAccounts)
244 {
245 context.netOps.subAccount(ispSub, accountIds, false);
246 JLOG(context.j.debug()) << "doSubscribe: accounts: " << accountIds.size();
247 }
248
249 if (context.params.isMember(jss::account_history_tx_stream))
250 {
251 if (!context.app.config().useTxTables())
252 return rpcError(RpcNotEnabled);
253
255 auto const& req = context.params[jss::account_history_tx_stream];
256 if (!req.isMember(jss::account) || !req[jss::account].isString())
258
259 auto const id = parseBase58<AccountID>(req[jss::account].asString());
260 if (!id)
262
263 // Charge the cap only when net-new, like the account branches. Not
264 // atomic here (subAccountHistory does its own dup-detecting insert), but
265 // a concurrent race adds at most one entry, so the overshoot is trivial.
266 std::size_t const historyCharge = ispSub->hasAccountHistorySubscription(*id) ? 0 : 1;
267 if (wouldExceedSubscriptionCap(ispSub, historyCharge, subscriptionCap))
268 return rpc::makeParamError("Too many subscriptions for this connection.");
269
270 if (auto result = context.netOps.subAccountHistory(ispSub, *id); result != RpcSuccess)
271 {
272 return rpcError(result);
273 }
274
275 jvResult[jss::warning] =
276 "account_history_tx_stream is an experimental feature and likely "
277 "to be removed in the future";
278 JLOG(context.j.debug()) << "doSubscribe: account_history_tx_stream: " << toBase58(*id);
279 }
280
281 if (context.params.isMember(jss::books))
282 {
283 if (!context.params[jss::books].isArray())
285
286 // Book subscriptions are tracked separately (OrderBookDB) and are not
287 // part of totalSubscriptionCount(), so they are not gated by the
288 // per-connection account cap. Each book entry is validated and
289 // subscribed below.
290 for (auto& j : context.params[jss::books])
291 {
292 if (!j.isObject() || !j.isMember(jss::taker_pays) || !j.isMember(jss::taker_gets) ||
293 !j[jss::taker_pays].isObjectOrNull() || !j[jss::taker_gets].isObjectOrNull())
295
296 Book book;
297
298 if (auto const err = rpc::parseSubUnsubJson(book.in, j, jss::taker_pays, context.j);
299 err != RpcSuccess)
300 return rpcError(err);
301
302 if (auto const err = rpc::parseSubUnsubJson(book.out, j, jss::taker_gets, context.j);
303 err != RpcSuccess)
304 return rpcError(err);
305
306 if (book.in == book.out)
307 {
308 JLOG(context.j.info()) << "taker_gets same as taker_pays.";
309 return rpcError(RpcBadMarket);
310 }
311
313
314 if (j.isMember(jss::taker))
315 {
316 if (!j[jss::taker].isString())
318 takerID = parseBase58<AccountID>(j[jss::taker].asString());
319 if (!takerID)
321 }
322
323 if (j.isMember(jss::domain))
324 {
325 uint256 domain;
326 if (!j[jss::domain].isString() || !domain.parseHex(j[jss::domain].asString()))
327 {
329 }
330
331 book.domain = domain;
332 }
333
334 if (!isConsistent(book))
335 {
336 JLOG(context.j.warn()) << "Bad market: " << book;
337 return rpcError(RpcBadMarket);
338 }
339
340 context.netOps.subBook(ispSub, book);
341
342 // both_sides is deprecated.
343 bool const both = (j.isMember(jss::both) && j[jss::both].asBool()) ||
344 (j.isMember(jss::both_sides) && j[jss::both_sides].asBool());
345
346 if (both)
347 context.netOps.subBook(ispSub, reversed(book));
348
349 // state_now is deprecated.
350 if ((j.isMember(jss::snapshot) && j[jss::snapshot].asBool()) ||
351 (j.isMember(jss::state_now) && j[jss::state_now].asBool()))
352 {
356 if (lpLedger)
357 {
360
361 auto add = [&](json::StaticString field) {
362 context.netOps.getBookPage(
363 lpLedger,
364 field == jss::asks ? reversed(book) : book,
365 takerID ? *takerID : noAccount(),
366 false,
368 jvMarker,
369 jvOffers);
370
371 if (jvResult.isMember(field))
372 {
373 json::Value& results(jvResult[field]);
374 for (auto const& e : jvOffers[jss::offers])
375 results.append(e);
376 }
377 else
378 {
379 jvResult[field] = jvOffers[jss::offers];
380 }
381 };
382
383 if (both)
384 {
385 add(jss::bids);
386 add(jss::asks);
387 }
388 else
389 {
390 add(jss::offers);
391 }
392 }
393 }
394 }
395 }
396
397 return jvResult;
398}
399
400} // namespace xrpl
Stream debug() const
Definition Journal.h:344
Stream info() const
Definition Journal.h:350
Stream trace() const
Severity stream access functions.
Definition Journal.h:338
Stream warn() const
Definition Journal.h:356
Lightweight wrapper to tag static string.
Definition json_value.h:48
Represents a JSON value.
Definition json_value.h:117
bool isArray() const
Value & append(Value const &value)
Append value to array at the end.
std::string asString() const
Returns the unquoted string value.
bool isMember(char const *key) const
Return true if the object has a member named key.
virtual Config & config()=0
constexpr bool parseHex(std::string_view sv)
Parse a hex string into a base_uint.
Definition base_uint.h:525
Specifies an order book.
Definition Book.h:28
std::optional< std::size_t > maxSubscriptionsPerConnection
bool useTxTables() const
virtual ErrorCodeI subAccountHistory(ref ispListener, AccountID const &account)=0
subscribe an account's new transactions and retrieve the account's historical transactions
virtual bool subTransactions(ref ispListener)=0
virtual bool subPeerStatus(ref ispListener)=0
virtual bool subServer(ref ispListener, json::Value &jvResult, bool admin)=0
virtual bool subConsensus(ref ispListener)=0
virtual void subAccount(ref ispListener, hash_set< AccountID > const &vnaAccountIDs, bool realTime)=0
virtual bool subBook(ref ispListener, Book const &)=0
virtual bool subValidations(ref ispListener)=0
virtual bool subRTTransactions(ref ispListener)=0
virtual bool subLedger(ref ispListener, json::Value &jvResult)=0
virtual bool subBookChanges(ref ispListener)=0
virtual pointer addRpcSub(std::string const &strUrl, ref rspEntry)=0
virtual bool subManifests(ref ispListener)=0
virtual pointer findRpcSub(std::string const &strUrl)=0
std::shared_ptr< InfoSub > pointer
Definition InfoSub.h:91
bool tryReserveAccountSubscriptions(hash_set< AccountID > const &proposedAccounts, hash_set< AccountID > const &normalAccounts, std::size_t cap)
Enforce the cap and reserve a request's net-new accounts, atomically.
void setApiVersion(unsigned int apiVersion)
std::shared_ptr< InfoSub > const & ref
Definition InfoSub.h:97
bool hasAccountHistorySubscription(AccountID const &account) const
Whether this connection already tracks an account-history for account.
std::shared_ptr< ReadView const > getPublishedLedger()
virtual void getBookPage(std::shared_ptr< ReadView const > &lpLedger, Book const &book, AccountID const &uTakerID, bool const bProof, unsigned int iLimit, json::Value const &jvMarker, json::Value &jvResult)=0
virtual JobQueue & getJobQueue()=0
virtual NetworkOPs & getOPs()=0
virtual LedgerMaster & getLedgerMaster()=0
virtual boost::asio::io_context & getIOContext()=0
T empty(T... args)
@ Object
object value (collection of name/value pairs).
Definition json_value.h:29
@ Null
'null' value
Definition json_value.h:22
Charge const kFeeMediumBurdenRpc
static constexpr LimitRange kBookOffers
Limits for the book_offers command.
json::Value makeParamError(std::string const &message)
Returns a new json object that indicates invalid parameters.
Definition ErrorCodes.h:231
ErrorCodeI parseSubUnsubJson(Asset &asset, json::Value const &params, json::StaticString const &name, beast::Journal j)
Parse subscribe/unsubscribe parameters.
hash_set< AccountID > parseAccountIds(json::Value const &jvArray)
Parses an array of account IDs from a JSON value.
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
@ RpcStreamMalformed
Definition ErrorCodes.h:109
@ RpcBadMarket
Definition ErrorCodes.h:80
@ RpcSuccess
Definition ErrorCodes.h:27
@ RpcActMalformed
Definition ErrorCodes.h:73
@ RpcDomainMalformed
Definition ErrorCodes.h:141
@ RpcNotEnabled
Definition ErrorCodes.h:42
@ RpcInvalidParams
Definition ErrorCodes.h:67
@ RpcNoPermission
Definition ErrorCodes.h:36
std::optional< AccountID > parseBase58(std::string const &s)
Parse AccountID from checked, base58 string.
std::string toBase58(AccountID const &v)
Convert AccountID to base58 checked string.
Definition AccountID.cpp:95
std::unordered_set< Value, Hash, Pred, Allocator > hash_set
std::shared_ptr< RPCSub > makeRPCSub(InfoSub::Source &source, boost::asio::io_context &ioContext, JobQueue &jobQueue, std::string const &strUrl, std::string const &strUsername, std::string const &strPassword, ServiceRegistry &registry)
Definition RPCSub.cpp:211
json::Value rpcError(ErrorCodeI iError)
Definition RPCErr.cpp:13
@ ADMIN
Definition Role.h:27
Book reversed(Book const &book)
Definition Book.cpp:30
constexpr std::size_t kMaxSubscriptionsPerConnection
Maximum number of subscriptions a single client connection may hold at once.
Definition InfoSub.h:35
AccountID const & noAccount()
A placeholder for empty accounts.
constexpr bool exceedsSubscriptionCap(std::size_t current, std::size_t additional, std::size_t cap=kMaxSubscriptionsPerConnection)
Whether adding additional subscriptions to a connection already holding current would exceed the cap.
Definition InfoSub.h:51
json::Value doSubscribe(rpc::JsonContext &)
Definition Subscribe.cpp:50
bool isConsistent(Asset const &asset)
Definition Asset.h:323
BaseUInt< 256 > uint256
Definition base_uint.h:580
T dynamic_pointer_cast(T... args)
T size(T... args)
resource::Charge & loadType
Definition Context.h:30
InfoSub::pointer infoSub
Definition Context.h:36
beast::Journal const j
Definition Context.h:28
NetworkOPs & netOps
Definition Context.h:31
unsigned int apiVersion
Definition Context.h:37
Application & app
Definition Context.h:29
json::Value params
Definition Context.h:51
T value_or(T... args)
T what(T... args)