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