rippled
Loading...
Searching...
No Matches
Directory_test.cpp
1#include <test/jtx.h>
2
3#include <xrpl/basics/random.h>
4#include <xrpl/ledger/BookDirs.h>
5#include <xrpl/ledger/Sandbox.h>
6#include <xrpl/protocol/Feature.h>
7#include <xrpl/protocol/Protocol.h>
8#include <xrpl/protocol/TER.h>
9#include <xrpl/protocol/jss.h>
10
11#include <algorithm>
12#include <limits>
13
14namespace xrpl {
15namespace test {
16
18{
19 // Map [0-15576] into a unique 3 letter currency code
22 {
23 // There are only 17576 possible combinations
24 BEAST_EXPECT(i < 17577);
25
26 std::string code;
27
28 for (int j = 0; j != 3; ++j)
29 {
30 code.push_back('A' + (i % 26));
31 i /= 26;
32 }
33
34 return code;
35 }
36
37 // Insert n empty pages, numbered [0, ... n - 1], in the
38 // specified directory:
39 void
41 {
42 for (std::uint64_t i = 0; i < n; ++i)
43 {
44 auto p = std::make_shared<SLE>(keylet::page(base, i));
45
46 p->setFieldV256(sfIndexes, STVector256{});
47
48 if (i + 1 == n)
49 p->setFieldU64(sfIndexNext, 0);
50 else
51 p->setFieldU64(sfIndexNext, i + 1);
52
53 if (i == 0)
54 p->setFieldU64(sfIndexPrevious, n - 1);
55 else
56 p->setFieldU64(sfIndexPrevious, i - 1);
57
58 sb.insert(p);
59 }
60 }
61
62 void
64 {
65 using namespace jtx;
66
67 auto gw = Account("gw");
68 auto USD = gw["USD"];
69 auto alice = Account("alice");
70 auto bob = Account("bob");
71
72 testcase("Directory Ordering (with 'SortedDirectories' amendment)");
73
74 Env env(*this);
75 env.fund(XRP(10000000), alice, gw);
76
77 std::uint32_t const firstOfferSeq{env.seq(alice)};
78 for (std::size_t i = 1; i <= 400; ++i)
79 env(offer(alice, USD(i), XRP(i)));
80 env.close();
81
82 // Check Alice's directory: it should contain one
83 // entry for each offer she added, and, within each
84 // page the entries should be in sorted order.
85 {
86 auto const view = env.closed();
87
88 std::uint64_t page = 0;
89
90 do
91 {
92 auto p = view->read(keylet::page(keylet::ownerDir(alice), page));
93
94 // Ensure that the entries in the page are sorted
95 auto const& v = p->getFieldV256(sfIndexes);
96 BEAST_EXPECT(std::is_sorted(v.begin(), v.end()));
97
98 // Ensure that the page contains the correct orders by
99 // calculating which sequence numbers belong here.
100 std::uint32_t const minSeq = firstOfferSeq + (page * dirNodeMaxEntries);
101 std::uint32_t const maxSeq = minSeq + dirNodeMaxEntries;
102
103 for (auto const& e : v)
104 {
105 auto c = view->read(keylet::child(e));
106 BEAST_EXPECT(c);
107 BEAST_EXPECT(c->getFieldU32(sfSequence) >= minSeq);
108 BEAST_EXPECT(c->getFieldU32(sfSequence) < maxSeq);
109 }
110
111 page = p->getFieldU64(sfIndexNext);
112 } while (page != 0);
113 }
114
115 // Now check the orderbook: it should be in the order we placed
116 // the offers.
117 auto book = BookDirs(*env.current(), Book({xrpIssue(), USD.issue(), std::nullopt}));
118 int count = 1;
119
120 for (auto const& offer : book)
121 {
122 count++;
123 BEAST_EXPECT(offer->getFieldAmount(sfTakerPays) == USD(count));
124 BEAST_EXPECT(offer->getFieldAmount(sfTakerGets) == XRP(count));
125 }
126 }
127
128 void
130 {
131 testcase("dirIsEmpty");
132
133 using namespace jtx;
134 auto const alice = Account("alice");
135 auto const bob = Account("bob");
136 auto const charlie = Account("charlie");
137 auto const gw = Account("gw");
138
139 Env env(*this);
140
141 env.fund(XRP(1000000), alice, charlie, gw);
142 env.close();
143
144 // alice should have an empty directory.
145 BEAST_EXPECT(dirIsEmpty(*env.closed(), keylet::ownerDir(alice)));
146
147 // Give alice a signer list, then there will be stuff in the directory.
148 env(signers(alice, 1, {{bob, 1}}));
149 env.close();
150 BEAST_EXPECT(!dirIsEmpty(*env.closed(), keylet::ownerDir(alice)));
151
152 env(signers(alice, jtx::none));
153 env.close();
154 BEAST_EXPECT(dirIsEmpty(*env.closed(), keylet::ownerDir(alice)));
155
156 std::vector<IOU> const currencies = [this, &gw]() {
158
159 c.reserve((2 * dirNodeMaxEntries) + 3);
160
161 while (c.size() != c.capacity())
162 c.push_back(gw[currcode(c.size())]);
163
164 return c;
165 }();
166
167 // First, Alices creates a lot of trustlines, and then
168 // deletes them in a different order:
169 {
170 auto cl = currencies;
171
172 for (auto const& c : cl)
173 {
174 env(trust(alice, c(50)));
175 env.close();
176 }
177
178 BEAST_EXPECT(!dirIsEmpty(*env.closed(), keylet::ownerDir(alice)));
179
180 std::shuffle(cl.begin(), cl.end(), default_prng());
181
182 for (auto const& c : cl)
183 {
184 env(trust(alice, c(0)));
185 env.close();
186 }
187
188 BEAST_EXPECT(dirIsEmpty(*env.closed(), keylet::ownerDir(alice)));
189 }
190
191 // Now, Alice creates offers to buy currency, creating
192 // implicit trust lines.
193 {
194 auto cl = currencies;
195
196 BEAST_EXPECT(dirIsEmpty(*env.closed(), keylet::ownerDir(alice)));
197
198 for (auto c : currencies)
199 {
200 env(trust(charlie, c(50)));
201 env.close();
202 env(pay(gw, charlie, c(50)));
203 env.close();
204 env(offer(alice, c(50), XRP(50)));
205 env.close();
206 }
207
208 BEAST_EXPECT(!dirIsEmpty(*env.closed(), keylet::ownerDir(alice)));
209
210 // Now fill the offers in a random order. Offer
211 // entries will drop, and be replaced by trust
212 // lines that are implicitly created.
213 std::shuffle(cl.begin(), cl.end(), default_prng());
214
215 for (auto const& c : cl)
216 {
217 env(offer(charlie, XRP(50), c(50)));
218 env.close();
219 }
220 BEAST_EXPECT(!dirIsEmpty(*env.closed(), keylet::ownerDir(alice)));
221 // Finally, Alice now sends the funds back to
222 // Charlie. The implicitly created trust lines
223 // should drop away:
224 std::shuffle(cl.begin(), cl.end(), default_prng());
225
226 for (auto const& c : cl)
227 {
228 env(pay(alice, charlie, c(50)));
229 env.close();
230 }
231
232 BEAST_EXPECT(dirIsEmpty(*env.closed(), keylet::ownerDir(alice)));
233 }
234 }
235
236 void
238 {
239 testcase("RIPD-1353 Empty Offer Directories");
240
241 using namespace jtx;
242 Env env(*this);
243
244 auto const gw = Account{"gateway"};
245 auto const alice = Account{"alice"};
246 auto const USD = gw["USD"];
247
248 env.fund(XRP(10000), alice, gw);
249 env.close();
250 env.trust(USD(1000), alice);
251 env(pay(gw, alice, USD(1000)));
252
253 auto const firstOfferSeq = env.seq(alice);
254
255 // Fill up three pages of offers
256 for (int i = 0; i < 3; ++i)
257 for (int j = 0; j < dirNodeMaxEntries; ++j)
258 env(offer(alice, XRP(1), USD(1)));
259 env.close();
260
261 // remove all the offers. Remove the middle page last
262 for (auto page : {0, 2, 1})
263 {
264 for (int i = 0; i < dirNodeMaxEntries; ++i)
265 {
266 env(offer_cancel(alice, firstOfferSeq + page * dirNodeMaxEntries + i));
267 env.close();
268 }
269 }
270
271 // All the offers have been cancelled, so the book
272 // should have no entries and be empty:
273 {
274 Sandbox sb(env.closed().get(), tapNONE);
275 uint256 const bookBase = getBookBase({xrpIssue(), USD.issue(), std::nullopt});
276
277 BEAST_EXPECT(dirIsEmpty(sb, keylet::page(bookBase)));
278 BEAST_EXPECT(!sb.succ(bookBase, getQualityNext(bookBase)));
279 }
280
281 // Alice returns the USD she has to the gateway
282 // and removes her trust line. Her owner directory
283 // should now be empty:
284 {
285 env.trust(USD(0), alice);
286 env(pay(alice, gw, alice["USD"](1000)));
287 env.close();
288 BEAST_EXPECT(dirIsEmpty(*env.closed(), keylet::ownerDir(alice)));
289 }
290 }
291
292 void
294 {
295 testcase("Empty Chain on Delete");
296
297 using namespace jtx;
298 Env env(*this);
299
300 auto const gw = Account{"gateway"};
301 auto const alice = Account{"alice"};
302 auto const USD = gw["USD"];
303
304 env.fund(XRP(10000), alice);
305 env.close();
306
307 constexpr uint256 base("fb71c9aa3310141da4b01d6c744a98286af2d72ab5448d5adc0910ca0c910880");
308
309 constexpr uint256 item("bad0f021aa3b2f6754a8fe82a5779730aa0bbbab82f17201ef24900efc2c7312");
310
311 {
312 // Create a chain of three pages:
313 Sandbox sb(env.closed().get(), tapNONE);
314 makePages(sb, base, 3);
315
316 // Insert an item in the middle page:
317 {
318 auto p = sb.peek(keylet::page(base, 1));
319 BEAST_EXPECT(p);
320
321 STVector256 v;
322 v.push_back(item);
323 p->setFieldV256(sfIndexes, v);
324 sb.update(p);
325 }
326
327 // Now, try to delete the item from the middle
328 // page. This should cause all pages to be deleted:
329 BEAST_EXPECT(sb.dirRemove(keylet::page(base, 0), 1, keylet::unchecked(item), false));
330 BEAST_EXPECT(!sb.peek(keylet::page(base, 2)));
331 BEAST_EXPECT(!sb.peek(keylet::page(base, 1)));
332 BEAST_EXPECT(!sb.peek(keylet::page(base, 0)));
333 }
334
335 {
336 // Create a chain of four pages:
337 Sandbox sb(env.closed().get(), tapNONE);
338 makePages(sb, base, 4);
339
340 // Now add items on pages 1 and 2:
341 {
342 auto p1 = sb.peek(keylet::page(base, 1));
343 BEAST_EXPECT(p1);
344
345 STVector256 v1;
346 v1.push_back(~item);
347 p1->setFieldV256(sfIndexes, v1);
348 sb.update(p1);
349
350 auto p2 = sb.peek(keylet::page(base, 2));
351 BEAST_EXPECT(p2);
352
353 STVector256 v2;
354 v2.push_back(item);
355 p2->setFieldV256(sfIndexes, v2);
356 sb.update(p2);
357 }
358
359 // Now, try to delete the item from page 2.
360 // This should cause pages 2 and 3 to be
361 // deleted:
362 BEAST_EXPECT(sb.dirRemove(keylet::page(base, 0), 2, keylet::unchecked(item), false));
363 BEAST_EXPECT(!sb.peek(keylet::page(base, 3)));
364 BEAST_EXPECT(!sb.peek(keylet::page(base, 2)));
365
366 auto p1 = sb.peek(keylet::page(base, 1));
367 BEAST_EXPECT(p1);
368 BEAST_EXPECT(p1->getFieldU64(sfIndexNext) == 0);
369 BEAST_EXPECT(p1->getFieldU64(sfIndexPrevious) == 0);
370
371 auto p0 = sb.peek(keylet::page(base, 0));
372 BEAST_EXPECT(p0);
373 BEAST_EXPECT(p0->getFieldU64(sfIndexNext) == 1);
374 BEAST_EXPECT(p0->getFieldU64(sfIndexPrevious) == 1);
375 }
376 }
377
378 void
380 {
381 testcase("fixPreviousTxnID");
382 using namespace jtx;
383
384 auto const gw = Account{"gateway"};
385 auto const alice = Account{"alice"};
386 auto const USD = gw["USD"];
387
388 auto ledger_data = [this](Env& env) {
389 Json::Value params;
390 params[jss::type] = jss::directory;
391 params[jss::ledger_index] = "validated";
392 auto const result = env.rpc("json", "ledger_data", to_string(params))[jss::result];
393 BEAST_EXPECT(!result.isMember(jss::marker));
394 return result;
395 };
396
397 // fixPreviousTxnID is disabled.
398 Env env(*this, testable_amendments() - fixPreviousTxnID);
399 env.fund(XRP(10000), alice, gw);
400 env.close();
401 env.trust(USD(1000), alice);
402 env(pay(gw, alice, USD(1000)));
403 env.close();
404
405 {
406 auto const jrr = ledger_data(env);
407 auto const& jstate = jrr[jss::state];
408 BEAST_EXPECTS(checkArraySize(jstate, 2), jrr.toStyledString());
409 for (auto const& directory : jstate)
410 {
411 BEAST_EXPECT(directory["LedgerEntryType"] == jss::DirectoryNode); // sanity check
412 // The PreviousTxnID and PreviousTxnLgrSeq fields should not be
413 // on the DirectoryNode object when the amendment is disabled
414 BEAST_EXPECT(!directory.isMember("PreviousTxnID"));
415 BEAST_EXPECT(!directory.isMember("PreviousTxnLgrSeq"));
416 }
417 }
418
419 // Now enable the amendment so the directory node is updated.
420 env.enableFeature(fixPreviousTxnID);
421 env.close();
422
423 // Make sure the `PreviousTxnID` and `PreviousTxnLgrSeq` fields now
424 // exist
425 env(offer(alice, XRP(1), USD(1)));
426 auto const txID = to_string(env.tx()->getTransactionID());
427 auto const ledgerSeq = env.current()->header().seq;
428 env.close();
429 // Make sure the fields only exist if the object is touched
430 env(noop(gw));
431 env.close();
432
433 {
434 auto const jrr = ledger_data(env);
435 auto const& jstate = jrr[jss::state];
436 BEAST_EXPECTS(checkArraySize(jstate, 3), jrr.toStyledString());
437 for (auto const& directory : jstate)
438 {
439 BEAST_EXPECT(directory["LedgerEntryType"] == jss::DirectoryNode); // sanity check
440 if (directory[jss::Owner] == gw.human())
441 {
442 // gw's directory did not get touched, so it
443 // should not have those fields populated
444 BEAST_EXPECT(!directory.isMember("PreviousTxnID"));
445 BEAST_EXPECT(!directory.isMember("PreviousTxnLgrSeq"));
446 }
447 else
448 {
449 // All of the other directories, including the order
450 // book, did get touched, so they should have those
451 // fields
452 BEAST_EXPECT(directory.isMember("PreviousTxnID") && directory["PreviousTxnID"].asString() == txID);
453 BEAST_EXPECT(
454 directory.isMember("PreviousTxnLgrSeq") &&
455 directory["PreviousTxnLgrSeq"].asUInt() == ledgerSeq);
456 }
457 }
458 }
459 }
460
461 void
463 {
464 using namespace test::jtx;
465 Account alice("alice");
466
467 auto const testCase = [&, this](FeatureBitset features, auto setup) {
468 using namespace test::jtx;
469
470 Env env(*this, features);
471 env.fund(XRP(20000), alice);
472 env.close();
473
474 auto const [lastPage, full] = setup(env);
475
476 // Populate root page and last page
477 for (int i = 0; i < 63; ++i)
478 env(credentials::create(alice, alice, std::to_string(i)));
479 env.close();
480
481 // NOTE, everything below can only be tested on open ledger because
482 // there is no transaction type to express what bumpLastPage does.
483
484 // Bump position of last page from 1 to highest possible
485 auto const res = directory::bumpLastPage(
486 env,
487 lastPage,
488 keylet::ownerDir(alice.id()),
489 [lastPage, this](ApplyView& view, uint256 key, std::uint64_t page) {
490 auto sle = view.peek({ltCREDENTIAL, key});
491 if (!BEAST_EXPECT(sle))
492 return false;
493
494 BEAST_EXPECT(page == lastPage);
495 sle->setFieldU64(sfIssuerNode, page);
496 // sfSubjectNode is not set in self-issued credentials
497 view.update(sle);
498 return true;
499 });
500 BEAST_EXPECT(res);
501
502 // Create one more credential
503 env(credentials::create(alice, alice, std::to_string(63)));
504
505 // Not enough space for another object if full
506 auto const expected = full ? ter{tecDIR_FULL} : ter{tesSUCCESS};
507 env(credentials::create(alice, alice, "foo"), expected);
508
509 // Destroy all objects in directory
510 for (int i = 0; i < 64; ++i)
511 env(credentials::deleteCred(alice, alice, alice, std::to_string(i)));
512
513 if (!full)
514 env(credentials::deleteCred(alice, alice, alice, "foo"));
515
516 // Verify directory is empty.
517 auto const sle = env.le(keylet::ownerDir(alice.id()));
518 BEAST_EXPECT(sle == nullptr);
519
520 // Test completed
521 env.close();
522 };
523
524 testCase(testable_amendments() - fixDirectoryLimit, [this](Env&) -> std::tuple<std::uint64_t, bool> {
525 testcase("directory full without fixDirectoryLimit");
526 return {dirNodeMaxPages - 1, true};
527 });
528 testCase(
530 [this](Env&) -> std::tuple<std::uint64_t, bool> {
531 testcase("directory not full with fixDirectoryLimit");
532 return {dirNodeMaxPages - 1, false};
533 });
534 testCase(
536 [this](Env&) -> std::tuple<std::uint64_t, bool> {
537 testcase("directory full with fixDirectoryLimit");
539 });
540 }
541
542 void
543 run() override
544 {
545 testDirectoryOrdering();
546 testDirIsEmpty();
547 testRipd1353();
548 testEmptyChain();
549 testPreviousTxnID();
550 testDirectoryFull();
551 }
552};
553
554BEAST_DEFINE_TESTSUITE_PRIO(Directory, ledger, xrpl, 1);
555
556} // namespace test
557} // namespace xrpl
T capacity(T... args)
Represents a JSON value.
Definition json_value.h:130
A testsuite class.
Definition suite.h:51
testcase_t testcase
Memberspace for declaring test cases.
Definition suite.h:147
Writeable view to a ledger, for applying a transaction.
Definition ApplyView.h:114
virtual void update(std::shared_ptr< SLE > const &sle)=0
Indicate changes to a peeked SLE.
bool dirRemove(Keylet const &directory, std::uint64_t page, uint256 const &key, bool keepRoot)
Remove an entry from a directory.
Specifies an order book.
Definition Book.h:16
void push_back(uint256 const &v)
Discardable, editable view to a ledger.
Definition Sandbox.h:15
void update(std::shared_ptr< SLE > const &sle) override
Indicate changes to a peeked SLE.
std::optional< key_type > succ(key_type const &key, std::optional< key_type > const &last=std::nullopt) const override
Return the key of the next state item.
std::shared_ptr< SLE > peek(Keylet const &k) override
Prepare to modify the SLE associated with key.
void insert(std::shared_ptr< SLE > const &sle) override
Insert a new state SLE.
Immutable cryptographic account descriptor.
Definition Account.h:19
AccountID id() const
Returns the Account ID.
Definition Account.h:87
A transaction testing environment.
Definition Env.h:97
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:97
std::shared_ptr< ReadView const > closed()
Returns the last closed ledger.
Definition Env.cpp:91
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:260
void enableFeature(uint256 const feature)
Definition Env.cpp:616
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:239
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:283
std::shared_ptr< STTx const > tx() const
Return the tx data for the last JTx.
Definition Env.cpp:472
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:297
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:15
T is_same_v
T is_sorted(T... args)
T max(T... args)
Keylet unchecked(uint256 const &key) noexcept
Any ledger entry.
Definition Indexes.cpp:319
Keylet ownerDir(AccountID const &id) noexcept
The root page of an account's directory.
Definition Indexes.cpp:325
Keylet child(uint256 const &key) noexcept
Any item that can be in an owner dir.
Definition Indexes.cpp:166
Keylet page(uint256 const &root, std::uint64_t index=0) noexcept
A page in a directory.
Definition Indexes.cpp:331
Json::Value deleteCred(jtx::Account const &acc, jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:37
Json::Value create(jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:13
auto bumpLastPage(Env &env, std::uint64_t newLastPage, Keylet directory, std::function< bool(ApplyView &, uint256, std::uint64_t)> adjust) -> Expected< void, Error >
Move the position of the last page in the user's directory on open ledger to newLastPage.
Definition directory.cpp:11
Json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:13
Json::Value signers(Account const &account, std::uint32_t quorum, std::vector< signer > const &v)
Definition multisign.cpp:15
XRP_t const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:90
Json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:11
Json::Value offer_cancel(Account const &account, std::uint32_t offerSeq)
Cancel an offer.
Definition offer.cpp:23
static none_t const none
Definition tags.h:14
FeatureBitset testable_amendments()
Definition Env.h:54
bool checkArraySize(Json::Value const &val, unsigned int size)
Json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition offer.cpp:10
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:97
bool dirIsEmpty(ReadView const &view, Keylet const &k)
Returns true if the directory is empty.
Definition View.cpp:825
std::size_t constexpr dirNodeMaxEntries
The maximum number of entries per directory page.
Definition Protocol.h:37
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:597
std::uint64_t constexpr dirNodeMaxPages
The maximum number of pages allowed in a directory.
Definition Protocol.h:43
beast::xor_shift_engine & default_prng()
Return the default random engine.
uint256 getQualityNext(uint256 const &uBase)
Definition Indexes.cpp:119
uint256 getBookBase(Book const &book)
Definition Indexes.cpp:98
@ tapNONE
Definition ApplyView.h:11
@ tecDIR_FULL
Definition TER.h:268
@ tesSUCCESS
Definition TER.h:225
T push_back(T... args)
T shuffle(T... args)
T reserve(T... args)
T size(T... args)
void makePages(Sandbox &sb, uint256 const &base, std::uint64_t n)
void run() override
Runs the suite.
std::string currcode(std::size_t i)
T to_string(T... args)