Files
rippled/src/test/ledger/Directory_test.cpp

578 lines
18 KiB
C++

#include <test/jtx.h>
#include <xrpl/basics/random.h>
#include <xrpl/ledger/BookDirs.h>
#include <xrpl/ledger/Sandbox.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/jss.h>
#include <algorithm>
#include <limits>
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<SLE>(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<IOU> const currencies = [this, &gw]() {
std::vector<IOU> 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<std::uint64_t, bool> {
testcase("directory full without fixDirectoryLimit");
return {dirNodeMaxPages - 1, true};
});
testCase(
testable_amendments(), //
[this](Env&) -> std::tuple<std::uint64_t, bool> {
testcase("directory not full with fixDirectoryLimit");
return {dirNodeMaxPages - 1, false};
});
testCase(
testable_amendments(), //
[this](Env&) -> std::tuple<std::uint64_t, bool> {
testcase("directory full with fixDirectoryLimit");
return {std::numeric_limits<std::uint64_t>::max(), true};
});
}
void
run() override
{
testDirectoryOrdering();
testDirIsEmpty();
testRipd1353();
testEmptyChain();
testPreviousTxnID();
testDirectoryFull();
}
};
BEAST_DEFINE_TESTSUITE_PRIO(Directory, ledger, ripple, 1);
} // namespace test
} // namespace ripple