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