-
Notifications
You must be signed in to change notification settings - Fork 36
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
Lazy ownership tree based on pallet-unique's owner #27
Comments
A very important fact we should always keep in mind: In Substrate runtime, the IO access is so expensive that the size of the storage item and the computation usually doesn't matter. More small value access is much worse than fewer large value access (pretty similar to HDD access in dbms). Therefore by removing the recursive traversal in |
Excellent, thank you, well thought out. Regarding nested children, we should burn recursively - children should burn with their parent. Given that in most cases there will only be a single layer of children, no more than 1 level deep, this should be very cheap to execute. It is an expected operation, and you could in theory prevent someone from using an item (i.e. burn-to-use) by spamming it with children if we were to block burns by reference counters for example. The reason for not moving all children to owner / EOA automatically is that there is no easy way to then handle pending children (those sent into an NFT by non-owner enter an initial pending state before being ACCEPTed by the parent NFT owner) or non-transferables, e.g. an avatar which has a non-transferable experience card with certain levels and skills. This card has no business existing outside of this avatar, and should burn alongside him. |
Thanks for the analysis. Thinking about burning recursively, would it be possible just to burn the parent itself and create "bastard" children, and implement this by adding an Option to the lookup root function with a check for existence prior to each recursion (returning None)? |
Would there be a case where an avatar can have a mix of transferable/non-transferable child NFTs & in the event that an avatar is burned, we would only want to burn non-transferable NFTs, but release the other transferable child NFTs? Would we want to have a property to prevent certain child NFTs from being burnt with the parent or would that introduce complexity to the design? |
Hello! Just chiming in. I have an intuition that the rmrk core could benefit from having something of a reference system? A child of a NFT could either be a direct member or a borrowed reference. Burning a NFT would burn direct children but not borrowed references? This could also be a nice primitive to have in the runtime to support lending out NFTs? #[pallet::storage]
#[pallet::getter(fn children)]
/// Stores nft info
pub type Children<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
CollectionId,
Twox64Concat,
NftId,
Vec<(CollectionId, NFTReference)>,
>;
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo)]
enum NFTReference {
Lit(NftId),
Ref(NftId),
} Dipping my toes in rust and substrate land, so be nice plz 😅 |
@Swader Thanks. I've updated the proposal to consider recursive burning by default. @bmacer Brilliant idea! I love it a lot. By burning the target nft, all its children becomes "rootless" -- if we trace back its root owner, we will walk to a The only concern is that it will make tracking the current number of the nft harder. We can still track, but the dirty nodes will also be counted in. Not sure if this is a big problem though. |
It doesn't introduce a lot of complexity, if the lock is just like a safeguard for accidental burning. However on blockchain we usually don't consider the human mistakes. It's better to handle that by the frontend / sdk. So we don't need to pay even a bit of the engineering overhead. Non-transferable NFTs will be more troublesome. If we cannot deal with it properly at the pallet-uniques side, an immediate problem I can come up is that a user may transfer the token directly at pallet-uniques to bypass all the RMRK restrictions |
Closing via #31 |
An important feature of RMRK spec is to allow any NFT to own other NFTs. This is called nested NFTs. It makes NFTs composable. Typical use cases are:
Current approach
Now the basic NFT is implemented by
pallet-unique
. On top of it,RMRKCore
offers the advanced feature including the nested NFT. The basic logic can be described as below:AccountId
orCollectionIdAndNftTuple
(Nft
for short)AccountId
orNft
AccountId
, both rmrk-owner and unique-owner are set to the new owner. Then it recursively set unique-owenr of all its children to the new owner.Nft
, the rmrk-owner is set to the dest NFT, the unique-owner is set to the unique-owner of the dest NFT. Then it recursively set the unique-owenr of all its children to the the same account.This approach is suboptimal in three aspects:
rmrk-owner
andunique-owner
is theoretically redundant -- the former is a superset of the later. It increases the difficulty to maintain both relationship in sync.So here, we'd prefer an approach that can minimize the redundancy of the data and reduce the complexity and the overhead to maintain the data structure.
Proposal
The proposal is simple: use the ownership in
pallet-unique
to trace the hierarchy of the NFTs.The ownership in
pallet-unique
is represented byAccountId
. At the first glance, it doesn't fit our requirement which is to allow a NFT to own other NFTs. However theAccountId
isn't necessary to be a real wallet. It's just an address that can be backed by anything. In our case, we can create "virtual account" for each NFT instance by mapping their(CollectionId, NftId)
tuple to anAccountId
via hashing.Once each NFT has a virtual
AccountId
associated, we can safely establish the hierarchy withpallet-unique
's builtin ownership:AccountId
, just call unique's transferAccountId
, and then we do the same transfer as above]In this way, each operation is O(1) except the ownership check (discussed below). Even if you do a transfer of a NFT with heavy children, or put it to a sale, only the ownership of the top one needs to be changed.
The virtual account trick
This is a common trick widely used in Substrate. The virtual accounts are allocated to on-chain multisig wallets, anonymous proxies, and even some pallets. The benefit is that the
AccountId
unifies the identity representation for all kinds of entities, It can fit to any place that accepts an account to manage ownership.The virtual accounts are usually just hard-coded string or hashes without any private key associated. For example, the
AccountId
of a multisig wallet can be generated byhash('multisig' ++ encode([owners])
. Similarly, we can generate the virtual account id for each NFT like below:In the above example, "RmrkNft/" takes 8 bytes, and if AccountId is 32 bytes, we got 24 bytes entropy left for the hash of the nft ids.
Or if we are sure we can get an address space large enough to directly include the ids (e.g. AccountId32 can include CollectionId + Nft Id, which is just 8 bytes), we can just encode the ids to the account id directly. This enables reverse lookup much easier:
Ownership check
A drawback of this approach is that given a NFT in a tree, you cannot easily find if the user is owner if the tree. This can be solved by a reverse lookup:
decode_nft_account_id
is available)In this way, a lookup is O(h) where h is the depth of the NFT in the tree. It can be described in pseudocode:
Children
When burning a NFT itself, we need to ensure the is has no child. This can be done by either
It's easy to recursively burn all the children. To track the children of a NFT, a straightforward solution is to keep a record of all its children, and update the list dynamically when adding or removing a child:
Caveat: Note that this essentially builds an index for the ownership of the nfts. It can only get updated if the change of the ownership was initiated by the RMRK pallet. A bad case is that if a user transfer a NFT to another nft via pallet-uniques directly, the RMRK pallet will know nothing about this ownership change. The children index will be out-of-sync, and therefore when burning the parent nft, some children may be missed.
A solution is to somehow disallow pallet-uniques to transfer an NFT to another NFT. Or we can add a notification handler to pallet-uniques to call back whenever a transfer was done.
Pseudocode
Update notes
pallet-uniques
The text was updated successfully, but these errors were encountered: