0

Consider I mint 10 CryptoDoggy NFTs, each with an incrementing ID, starting at 0. I also have a uint => uint mapping, which records the position of the NFT using its ID as a key. Initially, its position will be equal to its ID. So the 5th CryptoDoggy I mint will have ID 4 and position 4.

Now, say that I wish to burn the CryptoDoggy with ID 5. The CryptoDoggy with ID 6 is now in position 5, and likewise with all IDs above 5.

But, updating the positions by looping over the whole mapping is not an option if CryptoDoggy creation is unbounded.

So how, for example, would I get the position of the CryptoDoggy with ID 8, without looping over anything unbounded?

2 Answers 2

1

You have to separate concerns:

  1. Confirming existence.
  2. Random access.
  3. Counting.
  4. Iterating.
  5. Deleting.
  6. Sorting.

This is not a database system, so hidden assumptions about data organization will mislead you. You're 100% responsible for working it out appropriately. A rookie mistake is overloading the contracts with external concerns.

Deleting from an unordered list without iteration is accomplished by moving the last item into the row to delete and then shortening the list. That can be combined with unique IDs (enforced) and random access to arbitrary structs.

Sorting is a separate concern. Before you make the mistake of implementing it when it isn't needed, have a look at this: https://medium.com/solidified/the-joy-of-minimalism-in-smart-contract-design-b67fb4073422. It will, hopefully, challenge assumptions about what you need.

If you still want a sorted list, consider:

It's important to understand how the patterns scale and the strategies they use control gas cost. Chose wisely.

Hope it helps.

7
  • Rob, thank you very much for your post (apologies for the delay in replying, only just seen it). I read your medium article about minimalism, it was great- clearly the optimum approach is to find a solution that does not require sorting. To that end, please see this post, where I outline my true problem without trying to be coy about what I'm trying to build :) ethresear.ch/t/no-loss-ponzi-how-to-allocate-interest/7184/3 Commented Mar 25, 2020 at 11:44
  • The business rules are a little cognitively heavy for a quick and full analysis. As a general approach, keep in mind that you can create structures of arbitrary complexity in an NFT, e.g. timestamps for last reconciliation. Given a set of events, you can determine what happened after the previous reconciliation. The "events" could be blocks in a global state, e.g. monthly dividends everyone got, or gathered from a subtree of nodes unique to a given NFT. Commented Mar 26, 2020 at 6:07
  • You might have to combine a tree of logical organization for business rules, with other structures to determine the unprocessed transactions. currentState = reconciledState + unReconciledUpdates in the Amortized Work pattern. Commented Mar 26, 2020 at 6:10
  • You can used ordered lists, if needed. As a general fail-safe, include a way to POP one queued transaction at a time in case they cannot all be processed in one gulp, In all likelihood, there will be some iteration to process the unReconciledUpdates. You can reframe the problem as a) finding a way to ensure housekeeping happens frequently, to control gas cost creep and b) having a way to recover in the event that a) fails in unanticipated cases. Commented Mar 26, 2020 at 6:12
  • 1
    In case it can inspire new modes of thinking, have a look at this Order Statistics Tree. github.com/rob-Hitchens/OrderStatisticsTree It keeps an ordered list and can tell you any node's rank, e.g. 87.5 percentile and it does it without iteration over a set of arbitrary size. The method used is optimized for fixed (limited) cost. It doesn't have to be exactly the same for every txn but it has to controllable and acceptable in all cases. Commented Mar 26, 2020 at 6:18
1

Let's say you need to store uint value and string name for each item in your data-base:

contract MyContract {
    uint[] public list;

    struct Item {
        bool valid;
        uint index;
        uint value; // example
        string name; // example
    }

    mapping(uint => Item) public table;

    /* Update or insert an item */
    function upsert(uint ID, uint value, string name) external {
        Item storage item = table[ID];
        if (!item.valid) {
            item.valid = true;
            item.index = list.length;
            list.push(ID);
        }
        item.value = value; // example
        item.name = name; // example
    }

    /* Remove an item */
    function remove(uint ID) external {
        Item storage item = table[ID];
        require(item.index < list.length);
        require(ID == list[item.index]);
        uint lastItem = list[list.length - 1];
        table[lastItem].index = item.index;
        list[item.index] = lastItem;
        list.length -= 1;
        delete table[ID];
    }

    /* Get the total number of items */
    function count() external view returns (uint) {
        return list.length;
    }
}

As you can see, there is no looping in this code.

8
  • Thanks- but I think this only works if items are removed in the opposite order to which they are added, not if they are removed in a random order? I.e. Last In First Out. If I insert 10 items, with IDs incrementing from 0 to 9, then try and remove item with ID5, the item.index (which is what I am calling 'position' in my original post) is not changed for the items with IDs 6 or 7 etc? I would need item.index to change from 6 to 5 for item with ID 6, etc. Commented Mar 20, 2020 at 16:21
  • To be clear- I'm not just trying to get an updated count of total items, I am looking to figure out the position (or the index, to use your terminology) of each item. Commented Mar 20, 2020 at 16:22
  • 1
    @AndrewStanger: I think this only works if items are removed in the opposite order to which they are added - you're wrong. Commented Mar 20, 2020 at 16:30
  • 1
    @AndrewStanger: This code works on solc v0.4.x. I'm not ot sure what compiler version you're using, but I believe that it should work also on solc v0.5.x. Commented Mar 20, 2020 at 18:09
  • 1
    @AndrewStanger: Regarding your other question, when you remove an item, the contract essentially swaps it with the last item in the list and then removes the last item from the list (by decrementing the list length). Subsequently, once you remove an item, the list is no longer sorted by increasing order of IDs. There is no such requirement in your question either (there is not even a list in your question). And of course, the actual requirement in your question (no looping) is fulfilled. Commented Mar 20, 2020 at 18:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.