#include #include #include #include #include #include #include #include #include #include namespace ripple { namespace test { struct Directory_test : public beast::unit_test::suite { // Map [0-15576] into a unique 3 letter currency code std::string currcode(std::size_t i) { // There are only 17576 possible combinations BEAST_EXPECT(i < 17577); std::string code; for (int j = 0; j != 3; ++j) { code.push_back('A' + (i % 26)); i /= 26; } return code; } // Insert n empty pages, numbered [0, ... n - 1], in the // specified directory: void makePages(Sandbox& sb, uint256 const& base, std::uint64_t n) { for (std::uint64_t i = 0; i < n; ++i) { auto p = std::make_shared(keylet::page(base, i)); p->setFieldV256(sfIndexes, STVector256{}); if (i + 1 == n) p->setFieldU64(sfIndexNext, 0); else p->setFieldU64(sfIndexNext, i + 1); if (i == 0) p->setFieldU64(sfIndexPrevious, n - 1); else p->setFieldU64(sfIndexPrevious, i - 1); sb.insert(p); } } void testDirectoryOrdering() { using namespace jtx; auto gw = Account("gw"); auto USD = gw["USD"]; auto alice = Account("alice"); auto bob = Account("bob"); testcase("Directory Ordering (with 'SortedDirectories' amendment)"); Env env(*this); env.fund(XRP(10000000), alice, gw); std::uint32_t const firstOfferSeq{env.seq(alice)}; for (std::size_t i = 1; i <= 400; ++i) env(offer(alice, USD(i), XRP(i))); env.close(); // Check Alice's directory: it should contain one // entry for each offer she added, and, within each // page the entries should be in sorted order. { auto const view = env.closed(); std::uint64_t page = 0; do { auto p = view->read(keylet::page(keylet::ownerDir(alice), page)); // Ensure that the entries in the page are sorted auto const& v = p->getFieldV256(sfIndexes); BEAST_EXPECT(std::is_sorted(v.begin(), v.end())); // Ensure that the page contains the correct orders by // calculating which sequence numbers belong here. std::uint32_t const minSeq = firstOfferSeq + (page * dirNodeMaxEntries); std::uint32_t const maxSeq = minSeq + dirNodeMaxEntries; for (auto const& e : v) { auto c = view->read(keylet::child(e)); BEAST_EXPECT(c); BEAST_EXPECT(c->getFieldU32(sfSequence) >= minSeq); BEAST_EXPECT(c->getFieldU32(sfSequence) < maxSeq); } page = p->getFieldU64(sfIndexNext); } while (page != 0); } // Now check the orderbook: it should be in the order we placed // the offers. auto book = BookDirs( *env.current(), Book({xrpIssue(), USD.issue(), std::nullopt})); int count = 1; for (auto const& offer : book) { count++; BEAST_EXPECT(offer->getFieldAmount(sfTakerPays) == USD(count)); BEAST_EXPECT(offer->getFieldAmount(sfTakerGets) == XRP(count)); } } void testDirIsEmpty() { testcase("dirIsEmpty"); using namespace jtx; auto const alice = Account("alice"); auto const bob = Account("bob"); auto const charlie = Account("charlie"); auto const gw = Account("gw"); Env env(*this); env.fund(XRP(1000000), alice, charlie, gw); env.close(); // alice should have an empty directory. BEAST_EXPECT(dirIsEmpty(*env.closed(), keylet::ownerDir(alice))); // Give alice a signer list, then there will be stuff in the directory. env(signers(alice, 1, {{bob, 1}})); env.close(); BEAST_EXPECT(!dirIsEmpty(*env.closed(), keylet::ownerDir(alice))); env(signers(alice, jtx::none)); env.close(); BEAST_EXPECT(dirIsEmpty(*env.closed(), keylet::ownerDir(alice))); std::vector const currencies = [this, &gw]() { std::vector c; c.reserve((2 * dirNodeMaxEntries) + 3); while (c.size() != c.capacity()) c.push_back(gw[currcode(c.size())]); return c; }(); // First, Alices creates a lot of trustlines, and then // deletes them in a different order: { auto cl = currencies; for (auto const& c : cl) { env(trust(alice, c(50))); env.close(); } BEAST_EXPECT(!dirIsEmpty(*env.closed(), keylet::ownerDir(alice))); std::shuffle(cl.begin(), cl.end(), default_prng()); for (auto const& c : cl) { env(trust(alice, c(0))); env.close(); } BEAST_EXPECT(dirIsEmpty(*env.closed(), keylet::ownerDir(alice))); } // Now, Alice creates offers to buy currency, creating // implicit trust lines. { auto cl = currencies; BEAST_EXPECT(dirIsEmpty(*env.closed(), keylet::ownerDir(alice))); for (auto c : currencies) { env(trust(charlie, c(50))); env.close(); env(pay(gw, charlie, c(50))); env.close(); env(offer(alice, c(50), XRP(50))); env.close(); } BEAST_EXPECT(!dirIsEmpty(*env.closed(), keylet::ownerDir(alice))); // Now fill the offers in a random order. Offer // entries will drop, and be replaced by trust // lines that are implicitly created. std::shuffle(cl.begin(), cl.end(), default_prng()); for (auto const& c : cl) { env(offer(charlie, XRP(50), c(50))); env.close(); } BEAST_EXPECT(!dirIsEmpty(*env.closed(), keylet::ownerDir(alice))); // Finally, Alice now sends the funds back to // Charlie. The implicitly created trust lines // should drop away: std::shuffle(cl.begin(), cl.end(), default_prng()); for (auto const& c : cl) { env(pay(alice, charlie, c(50))); env.close(); } BEAST_EXPECT(dirIsEmpty(*env.closed(), keylet::ownerDir(alice))); } } void testRipd1353() { testcase("RIPD-1353 Empty Offer Directories"); using namespace jtx; Env env(*this); auto const gw = Account{"gateway"}; auto const alice = Account{"alice"}; auto const USD = gw["USD"]; env.fund(XRP(10000), alice, gw); env.close(); env.trust(USD(1000), alice); env(pay(gw, alice, USD(1000))); auto const firstOfferSeq = env.seq(alice); // Fill up three pages of offers for (int i = 0; i < 3; ++i) for (int j = 0; j < dirNodeMaxEntries; ++j) env(offer(alice, XRP(1), USD(1))); env.close(); // remove all the offers. Remove the middle page last for (auto page : {0, 2, 1}) { for (int i = 0; i < dirNodeMaxEntries; ++i) { env(offer_cancel( alice, firstOfferSeq + page * dirNodeMaxEntries + i)); env.close(); } } // All the offers have been cancelled, so the book // should have no entries and be empty: { Sandbox sb(env.closed().get(), tapNONE); uint256 const bookBase = getBookBase({xrpIssue(), USD.issue(), std::nullopt}); BEAST_EXPECT(dirIsEmpty(sb, keylet::page(bookBase))); BEAST_EXPECT(!sb.succ(bookBase, getQualityNext(bookBase))); } // Alice returns the USD she has to the gateway // and removes her trust line. Her owner directory // should now be empty: { env.trust(USD(0), alice); env(pay(alice, gw, alice["USD"](1000))); env.close(); BEAST_EXPECT(dirIsEmpty(*env.closed(), keylet::ownerDir(alice))); } } void testEmptyChain() { testcase("Empty Chain on Delete"); using namespace jtx; Env env(*this); auto const gw = Account{"gateway"}; auto const alice = Account{"alice"}; auto const USD = gw["USD"]; env.fund(XRP(10000), alice); env.close(); constexpr uint256 base( "fb71c9aa3310141da4b01d6c744a98286af2d72ab5448d5adc0910ca0c910880"); constexpr uint256 item( "bad0f021aa3b2f6754a8fe82a5779730aa0bbbab82f17201ef24900efc2c7312"); { // Create a chain of three pages: Sandbox sb(env.closed().get(), tapNONE); makePages(sb, base, 3); // Insert an item in the middle page: { auto p = sb.peek(keylet::page(base, 1)); BEAST_EXPECT(p); STVector256 v; v.push_back(item); p->setFieldV256(sfIndexes, v); sb.update(p); } // Now, try to delete the item from the middle // page. This should cause all pages to be deleted: BEAST_EXPECT(sb.dirRemove( keylet::page(base, 0), 1, keylet::unchecked(item), false)); BEAST_EXPECT(!sb.peek(keylet::page(base, 2))); BEAST_EXPECT(!sb.peek(keylet::page(base, 1))); BEAST_EXPECT(!sb.peek(keylet::page(base, 0))); } { // Create a chain of four pages: Sandbox sb(env.closed().get(), tapNONE); makePages(sb, base, 4); // Now add items on pages 1 and 2: { auto p1 = sb.peek(keylet::page(base, 1)); BEAST_EXPECT(p1); STVector256 v1; v1.push_back(~item); p1->setFieldV256(sfIndexes, v1); sb.update(p1); auto p2 = sb.peek(keylet::page(base, 2)); BEAST_EXPECT(p2); STVector256 v2; v2.push_back(item); p2->setFieldV256(sfIndexes, v2); sb.update(p2); } // Now, try to delete the item from page 2. // This should cause pages 2 and 3 to be // deleted: BEAST_EXPECT(sb.dirRemove( keylet::page(base, 0), 2, keylet::unchecked(item), false)); BEAST_EXPECT(!sb.peek(keylet::page(base, 3))); BEAST_EXPECT(!sb.peek(keylet::page(base, 2))); auto p1 = sb.peek(keylet::page(base, 1)); BEAST_EXPECT(p1); BEAST_EXPECT(p1->getFieldU64(sfIndexNext) == 0); BEAST_EXPECT(p1->getFieldU64(sfIndexPrevious) == 0); auto p0 = sb.peek(keylet::page(base, 0)); BEAST_EXPECT(p0); BEAST_EXPECT(p0->getFieldU64(sfIndexNext) == 1); BEAST_EXPECT(p0->getFieldU64(sfIndexPrevious) == 1); } } void testPreviousTxnID() { testcase("fixPreviousTxnID"); using namespace jtx; auto const gw = Account{"gateway"}; auto const alice = Account{"alice"}; auto const USD = gw["USD"]; auto ledger_data = [this](Env& env) { Json::Value params; params[jss::type] = jss::directory; params[jss::ledger_index] = "validated"; auto const result = env.rpc("json", "ledger_data", to_string(params))[jss::result]; BEAST_EXPECT(!result.isMember(jss::marker)); return result; }; // fixPreviousTxnID is disabled. Env env(*this, testable_amendments() - fixPreviousTxnID); env.fund(XRP(10000), alice, gw); env.close(); env.trust(USD(1000), alice); env(pay(gw, alice, USD(1000))); env.close(); { auto const jrr = ledger_data(env); auto const& jstate = jrr[jss::state]; BEAST_EXPECTS(checkArraySize(jstate, 2), jrr.toStyledString()); for (auto const& directory : jstate) { BEAST_EXPECT( directory["LedgerEntryType"] == jss::DirectoryNode); // sanity check // The PreviousTxnID and PreviousTxnLgrSeq fields should not be // on the DirectoryNode object when the amendment is disabled BEAST_EXPECT(!directory.isMember("PreviousTxnID")); BEAST_EXPECT(!directory.isMember("PreviousTxnLgrSeq")); } } // Now enable the amendment so the directory node is updated. env.enableFeature(fixPreviousTxnID); env.close(); // Make sure the `PreviousTxnID` and `PreviousTxnLgrSeq` fields now // exist env(offer(alice, XRP(1), USD(1))); auto const txID = to_string(env.tx()->getTransactionID()); auto const ledgerSeq = env.current()->info().seq; env.close(); // Make sure the fields only exist if the object is touched env(noop(gw)); env.close(); { auto const jrr = ledger_data(env); auto const& jstate = jrr[jss::state]; BEAST_EXPECTS(checkArraySize(jstate, 3), jrr.toStyledString()); for (auto const& directory : jstate) { BEAST_EXPECT( directory["LedgerEntryType"] == jss::DirectoryNode); // sanity check if (directory[jss::Owner] == gw.human()) { // gw's directory did not get touched, so it // should not have those fields populated BEAST_EXPECT(!directory.isMember("PreviousTxnID")); BEAST_EXPECT(!directory.isMember("PreviousTxnLgrSeq")); } else { // All of the other directories, including the order // book, did get touched, so they should have those // fields BEAST_EXPECT( directory.isMember("PreviousTxnID") && directory["PreviousTxnID"].asString() == txID); BEAST_EXPECT( directory.isMember("PreviousTxnLgrSeq") && directory["PreviousTxnLgrSeq"].asUInt() == ledgerSeq); } } } } void testDirectoryFull() { using namespace test::jtx; Account alice("alice"); auto const testCase = [&, this](FeatureBitset features, auto setup) { using namespace test::jtx; Env env(*this, features); env.fund(XRP(20000), alice); env.close(); auto const [lastPage, full] = setup(env); // Populate root page and last page for (int i = 0; i < 63; ++i) env(credentials::create(alice, alice, std::to_string(i))); env.close(); // NOTE, everything below can only be tested on open ledger because // there is no transaction type to express what bumpLastPage does. // Bump position of last page from 1 to highest possible auto const res = directory::bumpLastPage( env, lastPage, keylet::ownerDir(alice.id()), [lastPage, this]( ApplyView& view, uint256 key, std::uint64_t page) { auto sle = view.peek({ltCREDENTIAL, key}); if (!BEAST_EXPECT(sle)) return false; BEAST_EXPECT(page == lastPage); sle->setFieldU64(sfIssuerNode, page); // sfSubjectNode is not set in self-issued credentials view.update(sle); return true; }); BEAST_EXPECT(res); // Create one more credential env(credentials::create(alice, alice, std::to_string(63))); // Not enough space for another object if full auto const expected = full ? ter{tecDIR_FULL} : ter{tesSUCCESS}; env(credentials::create(alice, alice, "foo"), expected); // Destroy all objects in directory for (int i = 0; i < 64; ++i) env(credentials::deleteCred( alice, alice, alice, std::to_string(i))); if (!full) env(credentials::deleteCred(alice, alice, alice, "foo")); // Verify directory is empty. auto const sle = env.le(keylet::ownerDir(alice.id())); BEAST_EXPECT(sle == nullptr); // Test completed env.close(); }; testCase( testable_amendments() - fixDirectoryLimit, [this](Env&) -> std::tuple { testcase("directory full without fixDirectoryLimit"); return {dirNodeMaxPages - 1, true}; }); testCase( testable_amendments(), // [this](Env&) -> std::tuple { testcase("directory not full with fixDirectoryLimit"); return {dirNodeMaxPages - 1, false}; }); testCase( testable_amendments(), // [this](Env&) -> std::tuple { testcase("directory full with fixDirectoryLimit"); return {std::numeric_limits::max(), true}; }); } void run() override { testDirectoryOrdering(); testDirIsEmpty(); testRipd1353(); testEmptyChain(); testPreviousTxnID(); testDirectoryFull(); } }; BEAST_DEFINE_TESTSUITE_PRIO(Directory, ledger, ripple, 1); } // namespace test } // namespace ripple