xrpld
Loading...
Searching...
No Matches
BookChanges_test.cpp
1#include <test/jtx/Account.h>
2#include <test/jtx/Env.h>
3#include <test/jtx/WSClient.h>
4#include <test/jtx/amount.h>
5#include <test/jtx/domain.h>
6#include <test/jtx/offer.h>
7#include <test/jtx/paths.h>
8#include <test/jtx/pay.h>
9#include <test/jtx/permissioned_dex.h>
10#include <test/jtx/sendmax.h>
11
12#include <xrpld/rpc/BookChanges.h>
13
14#include <xrpl/basics/base_uint.h>
15#include <xrpl/basics/chrono.h>
16#include <xrpl/beast/hash/uhash.h>
17#include <xrpl/beast/unit_test/suite.h>
18#include <xrpl/json/json_value.h>
19#include <xrpl/json/to_string.h>
20#include <xrpl/ledger/Ledger.h>
21#include <xrpl/protocol/Feature.h>
22#include <xrpl/protocol/LedgerFormats.h>
23#include <xrpl/protocol/SField.h>
24#include <xrpl/protocol/STArray.h>
25#include <xrpl/protocol/STObject.h>
26#include <xrpl/protocol/STTx.h>
27#include <xrpl/protocol/Serializer.h>
28#include <xrpl/protocol/TxFormats.h>
29#include <xrpl/protocol/jss.h>
30
31#include <cstdint>
32#include <memory>
33#include <stdexcept>
34#include <string>
35#include <unordered_set>
36#include <utility>
37#include <vector>
38
39namespace xrpl::test {
40
42{
43public:
44 void
46 {
47 testcase("Specify well-known strings as ledger input");
48 jtx::Env env(*this);
49 json::Value params, resp;
50
51 // As per convention in XRPL, ledgers can be specified with strings
52 // "closed", "validated" or "current"
53 params["ledger"] = "validated";
54 resp = env.rpc("json", "book_changes", to_string(params));
55 BEAST_EXPECT(!resp[jss::result].isMember(jss::error));
56 BEAST_EXPECT(resp[jss::result][jss::status] == "success");
57 BEAST_EXPECT(resp[jss::result][jss::validated] == true);
58
59 params["ledger"] = "current";
60 resp = env.rpc("json", "book_changes", to_string(params));
61 BEAST_EXPECT(!resp[jss::result].isMember(jss::error));
62 BEAST_EXPECT(resp[jss::result][jss::status] == "success");
63 BEAST_EXPECT(resp[jss::result][jss::validated] == false);
64
65 params["ledger"] = "closed";
66 resp = env.rpc("json", "book_changes", to_string(params));
67 BEAST_EXPECT(!resp[jss::result].isMember(jss::error));
68 BEAST_EXPECT(resp[jss::result][jss::status] == "success");
69
70 // In the unit-test framework, requesting for "closed" ledgers appears
71 // to yield "validated" ledgers. This is not new behavior. It is also
72 // observed in the unit tests for the LedgerHeader class.
73 BEAST_EXPECT(resp[jss::result][jss::validated] == true);
74
75 // non-conventional ledger input should throw an error
76 params["ledger"] = "non_conventional_ledger_input";
77 resp = env.rpc("json", "book_changes", to_string(params));
78 BEAST_EXPECT(resp[jss::result].isMember(jss::error));
79 BEAST_EXPECT(resp[jss::result][jss::status] != "success");
80 }
81
82 void
84 {
86 "If ledger_hash or ledger_index is not specified, the behavior "
87 "must default to the `current` ledger");
88 jtx::Env env(*this);
89
90 // As per convention in XRPL, ledgers can be specified with strings
91 // "closed", "validated" or "current"
92 json::Value const resp = env.rpc("json", "book_changes", to_string(json::Value{}));
93 BEAST_EXPECT(!resp[jss::result].isMember(jss::error));
94 BEAST_EXPECT(resp[jss::result][jss::status] == "success");
95
96 // I dislike asserting the below statement, because its dependent on the
97 // unit-test framework BEAST_EXPECT(resp[jss::result][jss::ledger_index]
98 // == 3);
99 }
100
101 void
103 {
104 testcase("Domain Offer");
105 using namespace jtx;
106
107 FeatureBitset const all{
108 jtx::testableAmendments() | featurePermissionedDomains | featureCredentials |
109 featurePermissionedDEX};
110
111 Env env(*this, all);
112 PermissionedDEX const permDex(env);
113 auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = permDex;
114
115 auto wsc = makeWSClient(env.app().config());
116
117 env(offer(alice, XRP(10), USD(10)), Domain(domainID));
118 env.close();
119
120 env(pay(bob, carol, USD(10)), Path(~USD), Sendmax(XRP(10)), Domain(domainID));
121 env.close();
122
123 std::string const txHash{
124 env.tx()->getJson(JsonOptions::Values::None)[jss::hash].asString()};
125
126 json::Value const txResult = env.rpc("tx", txHash)[jss::result];
127 auto const ledgerIndex = txResult[jss::ledger_index].asInt();
128
129 json::Value jvParams;
130 jvParams[jss::ledger_index] = ledgerIndex;
131
132 auto jv = wsc->invoke("book_changes", jvParams);
133 auto jrr = jv[jss::result];
134
135 BEAST_EXPECT(jrr[jss::changes].size() == 1);
136 BEAST_EXPECT(jrr[jss::changes][0u][jss::domain].asString() == to_string(domainID));
137 }
138
139 void
141 {
142 testcase("book_changes skips overflowing rate");
143 using namespace jtx;
144
145 Env env(*this);
146 Account const gw{"gw"};
147 Account const iouGw{"iouGw"};
148
149 auto const big = MPT{gw.id(), 1};
150 auto const usd = iouGw["USD"];
151
152 // This metadata represents a partial MPT/IOU offer fill whose deltas
153 // make divide(deltaGets, deltaPays) overflow before MPTokensV2 skips
154 // the unrepresentable book-change rate.
155 STObject finalFields = STObject::makeInnerObject(sfFinalFields);
156 finalFields.setFieldU32(sfSequence, 1);
157 finalFields.setFieldAmount(sfTakerGets, big(1'800'000'000'000'000'000ull));
158 finalFields.setFieldAmount(sfTakerPays, usd(9));
159
160 STObject previousFields = STObject::makeInnerObject(sfPreviousFields);
161 previousFields.setFieldU32(sfSequence, 1);
162 previousFields.setFieldAmount(sfTakerGets, big(3'600'000'000'000'000'000ull));
163 previousFields.setFieldAmount(sfTakerPays, usd(18));
164
165 STObject modifiedOffer{sfModifiedNode};
166 modifiedOffer.setFieldU16(sfLedgerEntryType, ltOFFER);
167 modifiedOffer.setFieldObject(sfFinalFields, finalFields);
168 modifiedOffer.setFieldObject(sfPreviousFields, previousFields);
169
170 STArray affectedNodes{sfAffectedNodes};
171 affectedNodes.pushBack(std::move(modifiedOffer));
172
173 auto metadata = std::make_shared<STObject>(sfTransactionMetaData);
174 metadata->setFieldArray(sfAffectedNodes, affectedNodes);
175
176 auto tx = std::make_shared<STTx const>(ttOFFER_CREATE, [](STObject&) {});
177
178 auto const test = [&](std::unordered_set<UInt256, beast::Uhash<>> const& features) {
179 auto ledger = std::make_shared<Ledger>(
180 2,
182 Rules{features},
183 env.current()->fees(),
184 env.app().getNodeFamily());
185
186 auto txSerializer = std::make_shared<Serializer>();
187 tx->add(*txSerializer);
188
189 auto metaSerializer = std::make_shared<Serializer>();
190 metadata->add(*metaSerializer);
191
192 ledger->rawTxInsert(UInt256{1}, txSerializer, metaSerializer);
193 ledger->setImmutable();
194 ledger->setValidated();
195
196 try
197 {
198 auto const result =
200 BEAST_EXPECT(result[jss::type] == "bookChanges");
201 BEAST_EXPECT(result[jss::changes].size() == 0);
202 }
203 catch (std::overflow_error const&)
204 {
205 fail("Overflowing book-change rate shouldn't throw");
206 }
207 };
208
210 test(std::unordered_set<UInt256, beast::Uhash<>>{featureMPTokensV2});
211 }
212
213 // Build a ledger whose transactions are OfferCreates carrying the supplied
214 // consumed-offer deltas, then run computeBookChanges over it. Each pair is
215 // (TakerGets, TakerPays) fully consumed off a resting offer.
216 static json::Value
218 {
219 auto ledger = std::make_shared<Ledger>(
220 2,
223 env.current()->fees(),
224 env.app().getNodeFamily());
225
226 std::uint32_t seq = 0;
227 for (auto const& [gets, pays] : crossings)
228 {
229 ++seq;
230
231 STObject finalFields = STObject::makeInnerObject(sfFinalFields);
232 finalFields.setFieldU32(sfSequence, seq);
233 finalFields.setFieldAmount(sfTakerGets, STAmount{gets.asset()});
234 finalFields.setFieldAmount(sfTakerPays, STAmount{pays.asset()});
235
236 STObject previousFields = STObject::makeInnerObject(sfPreviousFields);
237 previousFields.setFieldU32(sfSequence, seq);
238 previousFields.setFieldAmount(sfTakerGets, gets);
239 previousFields.setFieldAmount(sfTakerPays, pays);
240
241 STObject modifiedOffer{sfModifiedNode};
242 modifiedOffer.setFieldU16(sfLedgerEntryType, ltOFFER);
243 modifiedOffer.setFieldObject(sfFinalFields, finalFields);
244 modifiedOffer.setFieldObject(sfPreviousFields, previousFields);
245
246 STArray affectedNodes{sfAffectedNodes};
247 affectedNodes.pushBack(std::move(modifiedOffer));
248
249 auto metadata = std::make_shared<STObject>(sfTransactionMetaData);
250 metadata->setFieldArray(sfAffectedNodes, affectedNodes);
251
252 STTx const tx{ttOFFER_CREATE, [](STObject&) {}};
253
254 auto txSerializer = std::make_shared<Serializer>();
255 tx.add(*txSerializer);
256
257 auto metaSerializer = std::make_shared<Serializer>();
258 metadata->add(*metaSerializer);
259
260 ledger->rawTxInsert(UInt256{seq}, txSerializer, metaSerializer);
261 }
262
263 ledger->setImmutable();
264 ledger->setValidated();
265
267 }
268
269 void
271 {
272 testcase("book_changes skips overflowing volume");
273 using namespace jtx;
274
275 Env env(*this);
276
277 // Two crossings in one book, accumulated by the `+=` in the tally's
278 // else branch. The rate is 1 either way, so the divide() guard is not
279 // what is under test here.
280 //
281 // MPT: kMaxMpTokenAmount is INT64_MAX, so two halves sum past it. The
282 // add is a raw int64 add, which wraps to a negative amount rather than
283 // throwing, and canonicalize() only bounds the magnitude -- so before
284 // the fix this reported a negative volume.
285 {
286 auto const mptA = MPT{Account{"gw"}.id(), 1};
287 auto const mptB = MPT{Account{"gw"}.id(), 2};
288 auto const half = 5'000'000'000'000'000'000ull; // 2 * half > INT64_MAX
289
290 auto const result =
291 bookChangesFor(env, {{mptA(half), mptB(half)}, {mptA(half), mptB(half)}});
292
293 BEAST_EXPECT(result[jss::type] == "bookChanges");
294 if (BEAST_EXPECT(result[jss::changes].size() == 1))
295 {
296 auto const& change = result[jss::changes][0u];
297 // The second crossing is dropped, so the first one's volume
298 // stands. Above all it must not be negative.
299 BEAST_EXPECT(change[jss::volume_a].asString() == std::to_string(half));
300 BEAST_EXPECT(change[jss::volume_b].asString() == std::to_string(half));
301 }
302 }
303
304 // IOU: the addition throws std::overflow_error once the summed
305 // exponent passes IOUAmount::kMaxExponent. Before the fix that
306 // escaped computeBookChanges entirely.
307 {
308 Account const gwA{"gwA"};
309 Account const gwB{"gwB"};
310 // Mantissa in range, exponent at the maximum: two of these sum to
311 // one exponent past it.
312 STAmount const bigA{gwA["USD"].issue(), UINT64_C(9'000'000'000'000'000), 80};
313 STAmount const bigB{gwB["EUR"].issue(), UINT64_C(9'000'000'000'000'000), 80};
314
315 try
316 {
317 auto const result = bookChangesFor(env, {{bigA, bigB}, {bigA, bigB}});
318 BEAST_EXPECT(result[jss::type] == "bookChanges");
319 BEAST_EXPECT(result[jss::changes].size() == 1);
320 }
321 catch (std::overflow_error const&)
322 {
323 fail("Overflowing book-change volume shouldn't throw");
324 }
325 }
326 }
327
328 void
329 run() override
330 {
333
337 // Note: Other aspects of the book_changes rpc are fertile grounds
338 // for unit-testing purposes. It can be included in future work
339 }
340};
341
343
344} // namespace xrpl::test
A testsuite class.
Definition suite.h:52
void fail(String const &reason, char const *file, int line)
Record a failure.
Definition suite.h:554
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Represents a JSON value.
Definition json_value.h:117
Int asInt() const
virtual Config & config()=0
std::chrono::time_point< NetClock > time_point
Definition chrono.h:48
Rules controlling protocol behavior.
Definition Rules.h:40
void pushBack(STObject const &object)
Definition STArray.h:212
void setFieldU32(SField const &field, std::uint32_t)
Definition STObject.cpp:743
void setFieldAmount(SField const &field, STAmount const &)
Definition STObject.cpp:803
void add(Serializer &s) const override
Definition STObject.cpp:123
void setFieldU16(SField const &field, std::uint16_t)
Definition STObject.cpp:737
static STObject makeInnerObject(SField const &name)
Definition STObject.cpp:79
void setFieldObject(SField const &field, STObject const &v)
Definition STObject.cpp:839
virtual Family & getNodeFamily()=0
static json::Value bookChangesFor(jtx::Env &env, std::vector< std::pair< STAmount, STAmount > > const &crossings)
void run() override
Runs the suite.
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
AccountID id() const
Returns the Account ID.
A transaction testing environment.
Definition Env.h:161
Application & app()
Definition Env.h:300
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
json::Value rpc(unsigned apiVersion, std::unordered_map< std::string, std::string > const &headers, std::string const &cmd, Args &&... args)
Execute an RPC command.
Definition Env.h:1058
std::shared_ptr< STTx const > tx() const
Return the tx data for the last JTx.
Definition Env.cpp:560
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:377
Converts to MPT Issue or STAmount.
Add a path.
Definition paths.h:47
Sets the SendMax on a JTx.
Definition sendmax.h:16
T make_shared(T... args)
API version numbers used in later API versions.
Definition ApiVersion.h:36
json::Value computeBookChanges(std::shared_ptr< L const > const &lpAccepted)
Definition BookChanges.h:41
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
FeatureBitset testableAmendments()
Definition Env.h:92
json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition offer.cpp:14
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
std::unique_ptr< WSClient > makeWSClient(Config const &cfg, bool v2, unsigned rpcVersion, std::unordered_map< std::string, std::string > const &headers)
Returns a client operating through WebSockets/S.
Definition WSClient.cpp:371
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
BaseUInt< 256 > Domain
Domain is a 256-bit hash representing a specific domain.
Definition UintTypes.h:59
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:657
BaseUInt< 256 > UInt256
Definition base_uint.h:580
T static_pointer_cast(T... args)
T to_string(T... args)