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

XLS-46: DynamicNFT #5048

Open
wants to merge 32 commits into
base: develop
Choose a base branch
from
Open

XLS-46: DynamicNFT #5048

wants to merge 32 commits into from

Conversation

tequdev
Copy link
Contributor

@tequdev tequdev commented Jun 18, 2024

Replace from #4838
Co-Author: @xVet

High Level Overview of Change

Spec: XLS-46d: Dynamic Non Fungible Tokens (dNFTs)

This Amendment adds functionality to update the URI of NFToken objects by adding:

NFTokenModify: Transactor to initiate the altering of the URI
lsfMutable: Flag to be set in a NFTokenMint transaction to allow NFTokenModify to execute successfully on given NFT.

Context of Change

XRPLF/XRPL-Standards#130

Type of Change

  • [x ] New feature (non-breaking change which adds functionality)
  • [x ] Tests (you added tests for code that already exists, or your new feature included in this PR)

API Impact

  • Public API: New feature (new methods and/or new fields)
  • Public API: Breaking change (in general, breaking changes should only impact the next api_version)
  • libxrpl change (any change that may affect libxrpl or dependents of libxrpl)
  • Peer protocol change (must be backward compatible or bump the peer protocol version)

Copy link

codecov bot commented Jun 18, 2024

Codecov Report

Attention: Patch coverage is 94.64286% with 3 lines in your changes missing coverage. Please review.

Project coverage is 76.1%. Comparing base (1fbf8da) to head (0f02668).

Files with missing lines Patch % Lines
src/xrpld/app/tx/detail/NFTokenUtils.cpp 88.2% 2 Missing ⚠️
src/xrpld/app/tx/detail/NFTokenModify.cpp 96.8% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##           develop   #5048     +/-   ##
=========================================
- Coverage     76.2%   76.1%   -0.0%     
=========================================
  Files          760     762      +2     
  Lines        61568   61620     +52     
  Branches      8126    8147     +21     
=========================================
+ Hits         46909   46923     +14     
- Misses       14659   14697     +38     
Files with missing lines Coverage Δ
include/xrpl/protocol/Feature.h 100.0% <ø> (ø)
include/xrpl/protocol/nft.h 100.0% <ø> (ø)
src/libxrpl/protocol/Feature.cpp 94.6% <ø> (ø)
src/libxrpl/protocol/TxFormats.cpp 100.0% <100.0%> (ø)
src/xrpld/app/tx/detail/NFTokenMint.cpp 98.0% <100.0%> (+<0.1%) ⬆️
src/xrpld/app/tx/detail/NFTokenModify.h 100.0% <100.0%> (ø)
src/xrpld/app/tx/detail/NFTokenUtils.h 100.0% <ø> (ø)
src/xrpld/app/tx/detail/applySteps.cpp 86.7% <100.0%> (+0.1%) ⬆️
src/xrpld/app/tx/detail/NFTokenModify.cpp 96.8% <96.8%> (ø)
src/xrpld/app/tx/detail/NFTokenUtils.cpp 91.2% <88.2%> (-0.2%) ⬇️

... and 10 files with indirect coverage changes

Impacted file tree graph

@shawnxie999
Copy link
Collaborator

In other PR, I suggested adding a check so that NFTokenModify would fail if there is any outstanding offers. This is to prevent the issuer from changing the URI and then accept an existing offer.

@tequdev
Copy link
Contributor Author

tequdev commented Jun 20, 2024

In other PR, I suggested adding a check so that NFTokenModify would fail if there is any outstanding offers. This is to prevent the issuer from changing the URI and then accept an existing offer.

As mentioned in this comment, wouldn't that make sense since the issuer can change the URI after the user has bought it?
Users need to be aware of the risk to mutable NFTs, just as they need to be aware of the possibility of being burned by the issuer after purchasing a Burnable NFT.

#4838 (comment)

@xVet
Copy link

xVet commented Jun 20, 2024

Correct, if you own a dynamic NFT then you agree that it can be changed by the issuer. I wouldn't personally put a check on offers, because this might cut into some use cases people come up with.

@shawnxie999
Copy link
Collaborator

BTW, the "Context of change" section linked to the XLS20 proposal, I think you meant to link to this one XRPLF/XRPL-Standards#130

@tequdev
Copy link
Contributor Author

tequdev commented Jun 22, 2024

BTW, the "Context of change" section linked to the XLS20 proposal, I think you meant to link to this one XRPLF/XRPL-Standards#130

good catch, fixed!

Copy link
Collaborator

@scottschurr scottschurr left a comment

Choose a reason for hiding this comment

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

Nice job on this pull request! The unit test coverage is pretty good. I appreciate that you took the time to do that.

I left quite a number of comments, but don't be dismayed by that ...

  1. Most of the comments are personal preferences of mine (although I justify them).
  2. I can be a very picky reviewer.

That said, I think that my comment regarding the nft::updateToken() implementation is pretty important. If you choose not to implement that change I'll want to understand why.

For your convenience I made commits that implement all of the changes I've suggested. If you want, you're welcome to cherry pick them, or you can just use them as examples of the changes I am suggesting. The commits are the top-most three on branch https://github.com/scottschurr/rippled/commits/tequ-featureDynamicNFT/

  • Suggested approach to NFTokenMintMask: 8aec32c
  • Suggested change for testNFTokenModify(): 813c302
  • Suggested changes to NFTokenModify.cpp and NFTokenUtils.*: 1393268

I hope you find those useful.

Thanks for the submission!

include/xrpl/protocol/TxFlags.h Outdated Show resolved Hide resolved
{
{sfNFTokenID, soeREQUIRED},
{sfOwner, soeOPTIONAL},
{sfURI, soeOPTIONAL},
Copy link
Collaborator

Choose a reason for hiding this comment

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

The spec (XRPLF/XRPL-Standards#130 August 18, 2023 version) specifies that the sfURI field is required. Here it is listed as soeOPTIONAL. I think that soeOPTIONAL is a good choice, because that matches the behavior of NFTokenMint. Perhaps the spec needs to be updated?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since you pointed it out, as of Jun 29 the sfURI field has been updated to an optional field.

Copy link
Collaborator

@shawnxie999 shawnxie999 Jul 3, 2024

Choose a reason for hiding this comment

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

Is there any use case for removing the URI from an existing NFT? Is there any difference from burning the NFT compared to an NFT without a URI?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Such a use case may not be common, but it is not impossible to say that there is no use case.

Since the NFTokenMint transaction can be executed with the URI field unspecified, it is reasonable that the NFTokenModify transaction can change the URI field to unspecified.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Makes sense. Thanks!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Should there be a flag for removing the URI from the NFT, like with NFTokenMinter, instead of just using its absence?

src/test/app/NFToken_test.cpp Outdated Show resolved Hide resolved
src/test/app/NFToken_test.cpp Show resolved Hide resolved
src/test/app/NFToken_test.cpp Outdated Show resolved Hide resolved
src/xrpld/app/tx/detail/NFTokenModify.cpp Outdated Show resolved Hide resolved
src/xrpld/app/tx/detail/NFTokenModify.cpp Outdated Show resolved Hide resolved
src/xrpld/app/tx/detail/NFTokenModify.cpp Outdated Show resolved Hide resolved
src/xrpld/app/tx/detail/NFTokenModify.cpp Outdated Show resolved Hide resolved
src/xrpld/app/tx/detail/NFTokenUtils.cpp Outdated Show resolved Hide resolved
@tequdev
Copy link
Contributor Author

tequdev commented Jun 29, 2024

@scottschurr
Thanks for the review. I fully agree with your suggestions and have cherry-picked all three commits.

Copy link
Collaborator

@scottschurr scottschurr left a comment

Choose a reason for hiding this comment

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

Looks great to me! Thanks for your patience with my review.

ApplyView& view,
AccountID const& owner,
uint256 const& nftokenID,
std::optional<ripple::Slice> const& uri)
Copy link
Collaborator

Choose a reason for hiding this comment

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

@scottschurr may I ask why is Slice type used here, and not Buffer?

Also another related question, in STBlob, it usesBuffer to store value_, but why value_type have type Slice and not Buffer? In STBlob:

class STBlob : public STBase, public CountedObject<STBlob>
{
    Buffer value_;

public:
    using value_type = Slice;

Thanks!

Copy link
Collaborator

Choose a reason for hiding this comment

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

@scottschurr may I ask why is Slice type used here, and not Buffer?

Sure. The simple answer is that I was looking for the most obvious type to pass in. The only call in the code base (currently) to nft::changeTokenURI looks like this:

    return nft::changeTokenURI(view(), owner, nftokenID, ctx_.tx[~sfURI]);

The awkward question is "What type does ctx_.tx[~sfURI] return?" I could dig through a bunch of templates in STObject and try to figure that out. But it's really easy to get lost in there. So in situations like this I usually rely on my compiler to tell me.

I picked a type that I'm pretty sure is not constructible or assignable from whatever ctx_.tx[~sfURI] returns. I don't remember the specific type I picked this time, but I often pick int*. Then I tell the compiler that I want to assign ctx_.tx[~sfURI] to an int*. It might look like this:

    [[maybe_unused]] int* junk = ctx_.tx[~sfURI];  // !!!! DEBUG !!!!

Then I compile hoping to get a useful error message out. I use clang, so I this case I got:

/Users/scott/Development/rippled/src/xrpld/app/tx/detail/NFTokenModify.cpp:87:27: error: no viable conversion from 'std::optional<std::decay_t<typename STBlob::value_type>>' (aka 'optional<ripple::Slice>') to 'int *'
    [[maybe_unused]] int* junk = ctx_.tx[~sfURI];

Way off at the end of the error message it tells me "no viable conversion from ... (aka optional<ripple::Slice>) to int *.

So the error message told me that, in order to avoid conversions, I needed to pass an optional<ripple::Slice> to changeTokenURI(). Once I had that information I removed the test code from the code base.

A Slice is really cheap to copy, so it often makes sense to pass a Slice by value. But I'm all the way up to an optional<ripple::Slice> which is not quite so obvious about pass-by-value or pass-by-const-ref. I was conservative and picked pass-by-const-ref.

Also another related question, in STBlob, it uses Buffer to store value_, but why value_type have type Slice and not Buffer?

That's a great question, and it's all about efficiency. If we look inside Buffer it holds onto the buffer contents like this:

    std::unique_ptr<std::uint8_t[]> p_;

It's not a shared_ptr, it's a unique_ptr. So the Buffer holds a unique_ptr to the only copy of the data. If you're not acquainted with unique_ptr, it's worth getting to know. Here's what CppReference has to say: https://en.cppreference.com/w/cpp/memory/unique_ptr

As CppReference notes, a unique_ptr is neither copy constructible or copy assignable. So we can't copy the Buffer. The way it holds its data does not allow us to do so. We have to get information about the contents of the Buffer some other way.

That way is called a Slice. If you look inside a Slice, you'll see that it only holds two pieces of data:

private:
    std::uint8_t const* data_ = nullptr;
    std::size_t size_ = 0;

We have a pointer to the const data (so we can't mess with the data inside the Buffer) and a length. If this code had been written with C++20, we'd probably be using the ranges library. A Slice looks very much like a ranges::contiguous_range. But this was done using C++11.

So Slice gives us a way to pass around information about the contents of the Buffer without actually copying Buffer or moving its contents around.

Why go to all this effort instead of putting the data inside of a shared_ptr and passing around shared_ptrs? We gain both efficiency and safety.

Efficiency

A shared_ptr requires atomic operations, which are compute expensive. If you copy a shared_ptr you have to increment its atomic count. And every time a shared_ptr is destroyed its atomic count is decremented. The unique_ptr has none of that overhead.

Safety
A shared_ptr is risky in two ways.

  1. First is that if you have multiple shared_ptrs to non-const data, then anybody with one of those shared_ptr can modify the contents of the Buffer. That leads to debugging mayhem if anyone doesn't follow rules closely. That's why a Slice holds a std::uint8_t const* data_. That const keeps anyone holding aSlice from messing with the contents of the Buffer.

  2. The other problem with shared_ptr is that the contents of the shared_ptr hang around until the last copy of that shared_ptr is destroyed. It is often difficult to know who has responsibility for destroying the contents of a shared_ptr. But a unique_ptr has none of that ambiguity.

With unique_ptr the lifetime is obvious. Whoever holds the unique_ptr has responsibility for the lifetime. If the lifetime needs to exceed the lifetime of the holding object, then the unique_ptr must be moved, so the originally holding object no longer has the contents of the unique_ptr.

That's a very long winded answer. I hope it made some sense.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Really appreciate the detailed response. Thanks Scott! ❤️

Copy link
Collaborator

@shawnxie999 shawnxie999 left a comment

Choose a reason for hiding this comment

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

Looks good!

Copy link
Collaborator

@yinyiqian1 yinyiqian1 left a comment

Choose a reason for hiding this comment

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

looks good

@Kassaking7 Kassaking7 mentioned this pull request Jul 15, 2024
9 tasks
@shawnxie999
Copy link
Collaborator

I suggest holding off on merging this PR until the Clio counterpart is also approved. While this isn't standard practice, the NFT functionality is in a unique position where its Clio implementation is necessary for it to work correctly. IMO ideally we should make sure the feature works on both ends before merging both into develop.

@tequdev
Copy link
Contributor Author

tequdev commented Sep 11, 2024

I suggest holding off on merging this PR until the Clio counterpart is also approved. While this isn't standard practice, the NFT functionality is in a unique position where its Clio implementation is necessary for it to work correctly. IMO ideally we should make sure the feature works on both ends before merging both into develop.

The PR has been inactive for about a month...
If it is not included in the next release (2.3.0), we will have to wait a few more months.

@cindyyan317
Copy link
Collaborator

I suggest holding off on merging this PR until the Clio counterpart is also approved. While this isn't standard practice, the NFT functionality is in a unique position where its Clio implementation is necessary for it to work correctly. IMO ideally we should make sure the feature works on both ends before merging both into develop.

The PR has been inactive for about a month... If it is not included in the next release (2.3.0), we will have to wait a few more months.

Hi @tequdev ,
The Clio counterpart has been finished. After this PR is merged to rippled, Clio will update the XRPL library and do the final test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants