Skip to content

Commit

Permalink
[FOLD] Add unit test demonstrating RPC problems from broken links
Browse files Browse the repository at this point in the history
  • Loading branch information
scottschurr committed Jan 17, 2024
1 parent a9da359 commit 1d8e6c0
Showing 1 changed file with 228 additions and 1 deletion.
229 changes: 228 additions & 1 deletion src/test/app/NFTokenBurn_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,8 @@ class NFTokenBurn0_test : public beast::unit_test::suite
// Otherwise either alice or minter can burn.
AcctStat& burner = owner.acct == becky.acct
? *(stats[acctDist(engine)])
: mintDist(engine) ? alice : minter;
: mintDist(engine) ? alice
: minter;

if (owner.acct == burner.acct)
env(token::burn(burner, nft));
Expand Down Expand Up @@ -1154,12 +1155,238 @@ class NFTokenBurn0_test : public beast::unit_test::suite
}
}

void
exerciseBrokenLinks(FeatureBitset features)
{
// Amendment fixLastNFTokenPageRemoval prevents the breakage we want
// to observe.
if (features[fixLastNFTokenPageRemoval])
return;

// a couple of directory merging scenarios that can only be tested by
// inserting and deleting in an ordered fashion. We do that testing
// now.
testcase("Exercise broken links");

using namespace test::jtx;

Account const alice{"alice"};
Account const minter{"minter"};

Env env{*this, features};
env.fund(XRP(1000), alice, minter);

// A lambda that generates 96 nfts packed into three pages of 32 each.
// Returns a sorted vector of the NFTokenIDs packed into the pages.
auto genPackedTokens = [this, &env, &alice, &minter]() {
std::vector<uint256> nfts;
nfts.reserve(96);

// We want to create fully packed NFT pages. This is a little
// tricky since the system currently in place is inclined to
// assign consecutive tokens to only 16 entries per page.
//
// By manipulating the internal form of the taxon we can force
// creation of NFT pages that are completely full. This lambda
// tells us the taxon value we should pass in in order for the
// internal representation to match the passed in value.
auto internalTaxon = [&env](
Account const& acct,
std::uint32_t taxon) -> std::uint32_t {
std::uint32_t tokenSeq =
env.le(acct)->at(~sfMintedNFTokens).value_or(0);

// If fixNFTokenRemint amendment is on, we must
// add FirstNFTokenSequence.
if (env.current()->rules().enabled(fixNFTokenRemint))
tokenSeq += env.le(acct)
->at(~sfFirstNFTokenSequence)
.value_or(env.seq(acct));

return toUInt32(
nft::cipheredTaxon(tokenSeq, nft::toTaxon(taxon)));
};

for (std::uint32_t i = 0; i < 96; ++i)
{
// In order to fill the pages we use the taxon to break them
// into groups of 16 entries. By having the internal
// representation of the taxon go...
// 0, 3, 2, 5, 4, 7...
// in sets of 16 NFTs we can get each page to be fully
// populated.
std::uint32_t const intTaxon = (i / 16) + (i & 0b10000 ? 2 : 0);
uint32_t const extTaxon = internalTaxon(minter, intTaxon);
nfts.push_back(
token::getNextID(env, minter, extTaxon, tfTransferable));
env(token::mint(minter, extTaxon), txflags(tfTransferable));
env.close();

// Minter creates an offer for the NFToken.
uint256 const minterOfferIndex =
keylet::nftoffer(minter, env.seq(minter)).key;
env(token::createOffer(minter, nfts.back(), XRP(0)),
txflags(tfSellNFToken));
env.close();

// alice accepts the offer.
env(token::acceptSellOffer(alice, minterOfferIndex));
env.close();
}

// Sort the NFTs so they are listed in storage order, not
// creation order.
std::sort(nfts.begin(), nfts.end());

// Verify that the ledger does indeed contain exactly three pages
// of NFTs with 32 entries in each page.
Json::Value jvParams;
jvParams[jss::ledger_index] = "current";
jvParams[jss::binary] = false;
{
Json::Value jrr = env.rpc(
"json",
"ledger_data",
boost::lexical_cast<std::string>(jvParams));

Json::Value& state = jrr[jss::result][jss::state];

int pageCount = 0;
for (Json::UInt i = 0; i < state.size(); ++i)
{
if (state[i].isMember(sfNFTokens.jsonName) &&
state[i][sfNFTokens.jsonName].isArray())
{
BEAST_EXPECT(
state[i][sfNFTokens.jsonName].size() == 32);
++pageCount;
}
}
// If this check fails then the internal NFT directory logic
// has changed.
BEAST_EXPECT(pageCount == 3);
}
return nfts;
};

// Generate three packed pages.
std::vector<uint256> nfts = genPackedTokens();
BEAST_EXPECT(nftCount(env, alice) == 96);
BEAST_EXPECT(ownerCount(env, alice) == 3);

// Verify that that all three pages are present and remember the
// indexes.
auto lastNFTokenPage = env.le(keylet::nftpage_max(alice));
if (!BEAST_EXPECT(lastNFTokenPage))
return;

uint256 const middleNFTokenPageIndex =
lastNFTokenPage->at(sfPreviousPageMin);
auto middleNFTokenPage = env.le(keylet::nftpage(
keylet::nftpage_min(alice), middleNFTokenPageIndex));
if (!BEAST_EXPECT(middleNFTokenPage))
return;

uint256 const firstNFTokenPageIndex =
middleNFTokenPage->at(sfPreviousPageMin);
auto firstNFTokenPage = env.le(
keylet::nftpage(keylet::nftpage_min(alice), firstNFTokenPageIndex));
if (!BEAST_EXPECT(firstNFTokenPage))
return;

// Sell all the tokens in the very last page back to minter.
std::vector<uint256> last32NFTs;
for (int i = 0; i < 32; ++i)
{
last32NFTs.push_back(nfts.back());
nfts.pop_back();

// alice creates an offer for the NFToken.
uint256 const aliceOfferIndex =
keylet::nftoffer(alice, env.seq(alice)).key;
env(token::createOffer(alice, last32NFTs.back(), XRP(0)),
txflags(tfSellNFToken));
env.close();

// minter accepts the offer.
env(token::acceptSellOffer(minter, aliceOfferIndex));
env.close();
}

// Removing the last token from the last page deletes alice's last
// page. This is a bug. The contents of the next-to-last page
// should have been moved into the last page.
lastNFTokenPage = env.le(keylet::nftpage_max(alice));
BEAST_EXPECT(!lastNFTokenPage);
BEAST_EXPECT(ownerCount(env, alice) == 2);

// The "middle" page is still present, but has lost the
// NextPageMin field.
middleNFTokenPage = env.le(keylet::nftpage(
keylet::nftpage_min(alice), middleNFTokenPageIndex));
if (!BEAST_EXPECT(middleNFTokenPage))
return;
BEAST_EXPECT(middleNFTokenPage->isFieldPresent(sfPreviousPageMin));
BEAST_EXPECT(!middleNFTokenPage->isFieldPresent(sfNextPageMin));

// Attempt to delete alice's account, but fail because she owns NFTs.
auto const acctDelFee{drops(env.current()->fees().increment)};
env(acctdelete(alice, minter),
fee(acctDelFee),
ter(tecHAS_OBLIGATIONS));
env.close();

// minter sells the last 32 NFTs back to alice.
for (uint256 nftID : last32NFTs)
{
// minter creates an offer for the NFToken.
uint256 const minterOfferIndex =
keylet::nftoffer(minter, env.seq(minter)).key;
env(token::createOffer(minter, nftID, XRP(0)),
txflags(tfSellNFToken));
env.close();

// alice accepts the offer.
env(token::acceptSellOffer(alice, minterOfferIndex));
env.close();
}
BEAST_EXPECT(ownerCount(env, alice) == 3); // Three NFTokenPages.

// alice has an NFToken directory with a broken link in the middle.
{
// Try the account_objects RPC command. Alice's account only shows
// two NFT pages even though she owns more.
Json::Value acctObjs = [&env, &alice]() {
Json::Value params;
params[jss::account] = alice.human();
return env.rpc("json", "account_objects", to_string(params));
}();
BEAST_EXPECT(!acctObjs.isMember(jss::marker));
BEAST_EXPECT(
acctObjs[jss::result][jss::account_objects].size() == 2);
}
{
// Try the account_nfts RPC command. It only returns 64 NFTs
// although alice owns 96.
Json::Value aliceNFTs = [&env, &alice]() {
Json::Value params;
params[jss::account] = alice.human();
params[jss::type] = "state";
return env.rpc("json", "account_nfts", to_string(params));
}();
BEAST_EXPECT(!aliceNFTs.isMember(jss::marker));
BEAST_EXPECT(
aliceNFTs[jss::result][jss::account_nfts].size() == 64);
}
}

void
testWithFeats(FeatureBitset features)
{
testBurnRandom(features);
testBurnSequential(features);
testBurnTooManyOffers(features);
exerciseBrokenLinks(features);
}

protected:
Expand Down

0 comments on commit 1d8e6c0

Please sign in to comment.