Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixNFTokenRemint: prevent NFT re-mint: #4406

Merged
merged 43 commits into from
Mar 20, 2023
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
e5d61cf
initial commit
shawnxie999 Jan 17, 2023
95f644f
wip
shawnxie999 Jan 18, 2023
60ab256
Add fixUnburnableNFToken feature (#4391)
ledhed2222 Jan 18, 2023
9561268
NFToken new sequence construct
shawnxie999 Jan 18, 2023
51fe860
conflict
shawnxie999 Feb 1, 2023
dc4f146
clang
shawnxie999 Feb 1, 2023
6211841
update test
shawnxie999 Feb 1, 2023
fa19aa2
[Amendment] Allow NFT to be burned when number of offers is greater t…
shawnxie999 Feb 2, 2023
6233a8c
Merge branch 'feature/nft-fixes' into nft-remint
shawnxie999 Feb 3, 2023
5425a32
[FOLD]minor comment
shawnxie999 Feb 3, 2023
a48a437
add const
shawnxie999 Feb 9, 2023
ce42b74
[Amendment] Fix 3 issues around NFToken offer acceptance (#4380)
ledhed2222 Feb 9, 2023
40c55e9
[Amendment] Prevent brokered sale of NFToken to owner: (#4403)
scottschurr Feb 10, 2023
9c3855b
Merge branch 'feature/nft-fixes' into nft-remint
shawnxie999 Feb 13, 2023
c95f9e8
[FOLD] clang
shawnxie999 Feb 13, 2023
1a276a0
Add fixUnburnableNFToken feature (#4391)
ledhed2222 Jan 18, 2023
31959db
Allow NFT to be burned when number of offers is greater than 500 (#4346)
shawnxie999 Feb 2, 2023
bdaeee2
Fix 3 issues around NFToken offer acceptance (#4380)
ledhed2222 Feb 9, 2023
df7eb5d
Prevent brokered sale of NFToken to owner: (#4403)
scottschurr Feb 10, 2023
0c77813
Only account specified as destination can settle through brokerage: (…
dangell7 Feb 13, 2023
05602af
Rename to fixNonFungibleTokensV1_2 and some cosmetic changes (#4419)
ledhed2222 Feb 13, 2023
99ff97f
Resolve conflicts
shawnxie999 Feb 13, 2023
d464543
Add remint tests and flag change
shawnxie999 Feb 13, 2023
4a26e80
resolve conflicts
shawnxie999 Feb 14, 2023
facf8f0
[FOLD] refactor sequence generation
shawnxie999 Feb 14, 2023
4ef2890
[FOLD] clang
shawnxie999 Feb 14, 2023
386ffec
[FOLD] newline
shawnxie999 Feb 14, 2023
53f3238
[FOLD] remove new lines
shawnxie999 Feb 14, 2023
8747f8e
[FOLD] clang
shawnxie999 Feb 14, 2023
c4d4487
[FOLD] reserve field values for amm and hooks
shawnxie999 Feb 14, 2023
d04f37f
[FOLD] function renaming
shawnxie999 Feb 15, 2023
9bfa3ed
[fold] remove auto and addressed some comments
shawnxie999 Mar 7, 2023
5a174b3
[fold]param renaming
shawnxie999 Mar 7, 2023
8041edd
[fold] refactor tests
shawnxie999 Mar 7, 2023
f874f0f
[fold] clang
shawnxie999 Mar 7, 2023
fdb7a92
[fold]readd comment
shawnxie999 Mar 8, 2023
a217f29
[fold] refactor tokenSeq code
shawnxie999 Mar 8, 2023
ae09383
[fold] fix comments
shawnxie999 Mar 8, 2023
ac8712e
udpate comment
shawnxie999 Mar 8, 2023
7d6644f
enable/disable `fixNFTokenRemint` flag in test
shawnxie999 Mar 16, 2023
dacbb52
[fold]calng
shawnxie999 Mar 16, 2023
acf0873
add test feature flag in burn test
shawnxie999 Mar 16, 2023
6461c58
[fold]clang
shawnxie999 Mar 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/ripple/app/tx/impl/DeleteAccount.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,23 @@ DeleteAccount::preclaim(PreclaimContext const& ctx)
if ((*sleAccount)[sfSequence] + seqDelta > ctx.view.seq())
return tecTOO_SOON;

// When fixNFTokenRemint is enabled, we don't allow an account to be
// deleted if <FirstNFTokenSequence + MintedNFTokens> is within 256 of the
// current ledger. This is to prevent having duplicate NFTokenIDs after
// account re-creation.
//
// Without this restriction, duplicate NFTokenIDs can be reproduced when
// authorized minting is involved. Because when the minter mints a NFToken,
// the issuer's sequence does not change. So when the issuer re-creates
// their account and mints a NFToken, it is possible that the
// NFTokenSequence of this NFToken is the same as the one that the
// authorized minter minted in a previous ledger.
if (ctx.view.rules().enabled(fixNFTokenRemint) &&
((*sleAccount)[~sfFirstNFTokenSequence].value_or(0) +
(*sleAccount)[~sfMintedNFTokens].value_or(0) + seqDelta >
ctx.view.seq()))
return tecTOO_SOON;
Comment on lines +217 to +232
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imagine someone issuing a million "Silly Billy" NFTs, with the first minted in ledger 77,777,777.

This check will prevent the issuer from deleting the account until, at the earliest, ledger 78,778,032; that's not gonna happen for around 4 million seconds, or around a month and a half.

Copy link
Collaborator Author

@shawnxie999 shawnxie999 Mar 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, unfortunately this check needs to be in. Without it, the scenario below produces duplicate NFTID:

  1. Alice's account sequence is at 1
  2. Bob is Alice's authorized minter
  3. Bob mints 500 NFTs for Alice (NFTs will have sequences 1 - 501, since NFT sequence is computed by FirstNFTokenSequence + MintedNFTokens)
  4. Alice deletes her account at ledger 257 (enforced by AccountDelete amendment)
  5. Alice re-creates her account at ledger 258
  6. Alice mints a NFT. FirstNFTokenSequence initialized to her account sequence (258), and MintedNFTokens to 0. This newly minted NFT would have a sequence number of 258, which is a duplicate of what she issued through authorized minting before she deleted her account.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand. My point was that this kind of test means it's hard for someone to know when their account can be deleted and, depending on the number of NFTs they issue. For some accounts (those which don't have FirstNFTokenSequence set) the answer is "immediately" and for others it's "well, sometime in the future."

Copy link
Collaborator Author

@shawnxie999 shawnxie999 Mar 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I don't think this will be a common issue, since this restriction is only applicable for authorized minting situations.

If the issuer has never issued an NFT through authorized minting, then, FirstNFTokenSequence + MintedNFTokens <= AccountRoot Sequence will always be true, which means the AccountDelete amendment (ledger > Account Sequence + 256) already contains the new restriction.


// Verify that the account does not own any objects that would prevent
// the account from being deleted.
Keylet const ownerDirKeylet{keylet::ownerDir(account)};
Expand Down
57 changes: 51 additions & 6 deletions src/ripple/app/tx/impl/NFTokenMint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -160,15 +160,60 @@ NFTokenMint::doApply()
// Should not happen. Checked in preclaim.
return Unexpected(tecNO_ISSUER);

// Get the unique sequence number for this token:
std::uint32_t const tokenSeq = (*root)[~sfMintedNFTokens].value_or(0);
if (!ctx_.view().rules().enabled(fixNFTokenRemint))
{
std::uint32_t const nextTokenSeq = tokenSeq + 1;
if (nextTokenSeq < tokenSeq)
return Unexpected(tecMAX_SEQUENCE_REACHED);
// Get the unique sequence number for this token:
std::uint32_t const tokenSeq =
(*root)[~sfMintedNFTokens].value_or(0);
{
std::uint32_t const nextTokenSeq = tokenSeq + 1;
if (nextTokenSeq < tokenSeq)
return Unexpected(tecMAX_SEQUENCE_REACHED);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so you're just checking for int overflow based on ledger seq? that seems unnecessary

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd leave the check as is, as it's basically free (from a computational point of view).


(*root)[sfMintedNFTokens] = nextTokenSeq;
}
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
ctx_.view().update(root);
return tokenSeq;
}

// With fixNFTokenRemint amendment enabled:
//
// If the issuer hasn't minted a NFT before, we must
// initialize sfFirstNFTokenSequence to equal to the current account
// sequence. In general, we must subtract account sequence by
// one, since it is incremented by the transactor beforehand. In
// scenarios of AuthorizedMinting or Tickets, we use the
// account sequence as it is because it has not been incremented.
//
// Whether we subtract the acct sequence by 1 is unimportant in real
// world use case. But it is needed in order to deterministically
// generate the NFTokenSequence for test cases.
if (auto fts = (*root)[~sfFirstNFTokenSequence]; !fts)
{
auto const accSeq = (*root)[sfSequence];

(*root)[sfMintedNFTokens] = nextTokenSeq;
(*root)[sfFirstNFTokenSequence] =
(*root)[~sfNFTokenMinter] == ctx_.tx[sfAccount] ||
ctx_.tx.getSeqProxy().isTicket()
? accSeq
: accSeq - 1;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have the right behavior, but I don't think the reasoning is correct. I suggest rewriting the comments and code like this:

        // With fixNFTokenRemint amendment enabled:
        //
        // If the issuer hasn't minted an NFToken before we must add a
        // FirstNFTokenSequence field to the issuer's AccountRoot.  The
        // value of the FirstNFTokenSequence must equal the issuer's
        // current account sequence.
        //
        // There are three situations:
        //  o If the first token is being minted by the issuer and
        //     * If the transaction consumes a Sequence number, then the
        //       Sequence has been pre-incremented by the time we get here in
        //       doApply.  We must decrement the value in the Sequence field.
        //     * Otherwise the transaction uses a Ticket so the Sequence has
        //       not been pre-incremented.  We use the Sequence value as is.
        //  o The first token is being minted by an authorized minter.  In
        //    this case the issuer's Sequence field has been left untouched.
        //    We use the issuer's Sequence value as is.
        if (!root->isFieldPresent(sfFirstNFTokenSequence))
        {
            auto const accSeq = root->at(sfSequence);

            root->at(sfFirstNFTokenSequence) =
                ctx_.tx.isFieldPresent(sfIssuer) ||
                    ctx_.tx.getSeqProxy().isTicket()
                ? accSeq
                : accSeq - 1;
        }

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what would be the benefit using using root->at(sfSequence) instead of (*root)[sfSequence]?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent question. There's no execution benefit. It's a subjective call.

Many folks find the (*root)[sfSequence] style of construct awkward to read. So, after we'd been putting up with that technique for a while, @ximinez added the STObject::at() method. I've looked at both ways of expressing what's going on and I find root->at(sfSequence) a bit easier to read. But there's no objective difference.

It's entirely your call for which would make the code easier for you to read when you maintain it in the future.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!


auto const mintedNftCnt = (*root)[~sfMintedNFTokens].value_or(0);
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved

(*root)[sfMintedNFTokens] = mintedNftCnt + 1;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integer literals are implicitly signed. A cautious approach to this line would be to add 1u.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just curious what benefit would i get from explicitly write it as an unsigned int?

Copy link
Collaborator

@scottschurr scottschurr Mar 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C++ does implicit type conversions between integers. That's something left over from C. If we know that mintedNftCnt is unsigned, then adding another unsigned number will not cause the result of the addition to become signed.

I don't have the integer conversion rules committed to memory; they are too complicated. Instead I just try to be very conservative when I'm dealing with integers that might overflow. If integer overflow might happen then everything possible needs to be done to make sure everything is unsigned. At least that's the way I approach it.

if ((*root)[sfMintedNFTokens] == 0)
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
return Unexpected(tecMAX_SEQUENCE_REACHED);

// Get the unique sequence number of this token by
// sfFirstNFTokenSequence + sfMintedNFTokens
auto const offset = (*root)[sfFirstNFTokenSequence];
auto const tokenSeq = offset + mintedNftCnt;
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved

// Check for more overflow cases
if (tokenSeq + 1 == 0 || tokenSeq < mintedNftCnt)
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
return Unexpected(tecMAX_SEQUENCE_REACHED);

ctx_.view().update(root);
return tokenSeq;
}();
Expand Down
3 changes: 2 additions & 1 deletion src/ripple/protocol/Feature.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 57;
static constexpr std::size_t numFeatures = 58;

/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated
Expand Down Expand Up @@ -344,6 +344,7 @@ extern uint256 const featureDisallowIncoming;
extern uint256 const featureXRPFees;
extern uint256 const fixUniversalNumber;
extern uint256 const fixNonFungibleTokensV1_2;
extern uint256 const fixNFTokenRemint;

} // namespace ripple

Expand Down
1 change: 1 addition & 0 deletions src/ripple/protocol/SField.h
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ extern SF_UINT32 const sfMintedNFTokens;
extern SF_UINT32 const sfBurnedNFTokens;
extern SF_UINT32 const sfHookStateCount;
extern SF_UINT32 const sfEmitGeneration;
extern SF_UINT32 const sfFirstNFTokenSequence;

// 64-bit integers (common)
extern SF_UINT64 const sfIndexNext;
Expand Down
1 change: 1 addition & 0 deletions src/ripple/protocol/impl/Feature.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ REGISTER_FEATURE(DisallowIncoming, Supported::yes, DefaultVote::no)
REGISTER_FEATURE(XRPFees, Supported::yes, DefaultVote::no);
REGISTER_FIX (fixUniversalNumber, Supported::yes, DefaultVote::no);
REGISTER_FIX (fixNonFungibleTokensV1_2, Supported::yes, DefaultVote::no);
REGISTER_FIX (fixNFTokenRemint, Supported::yes, DefaultVote::no);

// The following amendments have been active for at least two years. Their
// pre-amendment code has been removed and the identifiers are deprecated.
Expand Down
1 change: 1 addition & 0 deletions src/ripple/protocol/impl/LedgerFormats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ LedgerFormats::LedgerFormats()
{sfNFTokenMinter, soeOPTIONAL},
{sfMintedNFTokens, soeDEFAULT},
{sfBurnedNFTokens, soeDEFAULT},
{sfFirstNFTokenSequence, soeOPTIONAL},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note - this will require change to the SDKs I believe

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would you confirm this? doesn't block this PR but when a TX modifies this value and one of the libraries is trying to parse the metadata from that TX I think it will raise an exception

},
commonFields);

Expand Down
3 changes: 3 additions & 0 deletions src/ripple/protocol/impl/SField.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ CONSTRUCT_TYPED_SFIELD(sfMintedNFTokens, "MintedNFTokens", UINT32,
CONSTRUCT_TYPED_SFIELD(sfBurnedNFTokens, "BurnedNFTokens", UINT32, 44);
CONSTRUCT_TYPED_SFIELD(sfHookStateCount, "HookStateCount", UINT32, 45);
CONSTRUCT_TYPED_SFIELD(sfEmitGeneration, "EmitGeneration", UINT32, 46);
// Three field values of 47, 48 and 49 are reserved for
// LockCount(Hooks), VoteWeight(AMM), DiscountedFee(AMM)
CONSTRUCT_TYPED_SFIELD(sfFirstNFTokenSequence, "FirstNFTokenSequence", UINT32, 50);
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved

// 64-bit integers (common)
CONSTRUCT_TYPED_SFIELD(sfIndexNext, "IndexNext", UINT64, 1);
Expand Down
16 changes: 14 additions & 2 deletions src/test/app/NFTokenBurn_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -380,8 +380,20 @@ class NFTokenBurn_test : public beast::unit_test::suite
auto internalTaxon = [&env](
Account const& acct,
std::uint32_t taxon) -> std::uint32_t {
std::uint32_t const tokenSeq = {
env.le(acct)->at(~sfMintedNFTokens).value_or(0)};
std::uint32_t tokenSeq;

// If fixNFTokenRemint amendment is on, we must
// generate the NFT sequence using new construct
if (env.current()->rules().enabled(fixNFTokenRemint))
tokenSeq = {
env.le(acct)
->at(~sfFirstNFTokenSequence)
.value_or(env.seq(acct)) +
env.le(acct)->at(~sfMintedNFTokens).value_or(0)};
else
tokenSeq = {
env.le(acct)->at(~sfMintedNFTokens).value_or(0)};
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved

return toUInt32(
nft::cipheredTaxon(tokenSeq, nft::toTaxon(taxon)));
};
Expand Down
47 changes: 42 additions & 5 deletions src/test/app/NFTokenDir_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,16 @@ class NFTokenDir_test : public beast::unit_test::suite
Account const& account = accounts.emplace_back(
Account::base58Seed, std::string(seed));
env.fund(XRP(10000), account);
env.close();

// Should not advance ledger if fixNFTokenRemint is
// enabled. Because otherwise, accounts are initialized at
// different ledgers, and will have different account
// sequences, which then causes the sequence number of
// NFTokenIDs to be different
if (!features[fixNFTokenRemint])
env.close();
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
}
env.close();

// All of the accounts create one NFT and and offer that NFT to
// buyer.
Expand Down Expand Up @@ -408,8 +416,16 @@ class NFTokenDir_test : public beast::unit_test::suite
Account const& account = accounts.emplace_back(
Account::base58Seed, std::string(seed));
env.fund(XRP(10000), account);
env.close();

// Should not advance ledger if fixNFTokenRemint is
// enabled. Because otherwise, accounts are initialized at
// different ledgers, and will have different account
// sequences, which then causes the sequence number of
// NFTokenIDs to be different
if (!features[fixNFTokenRemint])
env.close();
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
}
env.close();

// All of the accounts create one NFT and and offer that NFT to
// buyer.
Expand Down Expand Up @@ -647,13 +663,23 @@ class NFTokenDir_test : public beast::unit_test::suite
// Create accounts for all of the seeds and fund those accounts.
std::vector<Account> accounts;
accounts.reserve(seeds.size());

// If fixNFTokenRemint is not enabled, accounts can be created in
// different ledgers.
// If fixNFTokenRemint is enabled, all accounts must be created in
// the same ledger in order to initialize all accounts with the same
// account sequence.
for (std::string_view const& seed : seeds)
{
Account const& account =
accounts.emplace_back(Account::base58Seed, std::string(seed));
env.fund(XRP(10000), account);
env.close();

// Only advance the ledger if fixNFTokenRemint is disabled
if (!features[fixNFTokenRemint])
env.close();
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
}
env.close();

// All of the accounts create one NFT and and offer that NFT to buyer.
std::vector<uint256> nftIDs;
Expand Down Expand Up @@ -822,13 +848,23 @@ class NFTokenDir_test : public beast::unit_test::suite
// Create accounts for all of the seeds and fund those accounts.
std::vector<Account> accounts;
accounts.reserve(seeds.size());

// If fixNFTokenRemint is not enabled, accounts can be created in
// different ledgers.
// If fixNFTokenRemint is enabled, accounts must be created in the
// same ledger in order to initialize all accounts with the same
// account sequence.
for (std::string_view const& seed : seeds)
{
Account const& account =
accounts.emplace_back(Account::base58Seed, std::string(seed));
env.fund(XRP(10000), account);
env.close();

// Only advance the ledger if fixNFTokenRemint is disabled
if (!features[fixNFTokenRemint])
env.close();
shawnxie999 marked this conversation as resolved.
Show resolved Hide resolved
}
env.close();

// All of the accounts create seven consecutive NFTs and and offer
// those NFTs to buyer.
Expand Down Expand Up @@ -1078,7 +1114,8 @@ class NFTokenDir_test : public beast::unit_test::suite
FeatureBitset const fixNFTDir{
fixNFTokenDirV1, featureNonFungibleTokensV1_1};

testWithFeats(all - fixNFTDir);
testWithFeats(all - fixNFTDir - fixNFTokenRemint);
testWithFeats(all - fixNFTokenRemint);
testWithFeats(all);
}
};
Expand Down
Loading