NFT Research - Attributes and Modifications

Further Open Discussion of NFTs

Starting with:

In this part I'd like to discuss attribute calculation, modification, and storage of NFT attributes, especially as they might relate to gaming.

Stats in Space

9,007,199,254,740,991 is a big number, but it's also a small number and we're going to discuss why. For starters this is the biggest integer than can be represented as a "number" in javascript. That also means its the largest number that can be handled by the "math" package in javascript. When stored in RAM its just 1 long 64 bit address which is just fine for for nearly all modern 64bit computers. However, when sent around in JSON and DB backups, its 16 bytes long. This is the space we are trying to minimize. We write this number as base64 and go from 16 bytes to 9 bytes. When represented as a base64 number it can look like this: V======== (depending on which glyphs you use to represent your base.). This is 9 bytes instead of 16, and while this is a savings almost to 50%, it can represent a much larger savings.

Screenshot from 2021-09-14 15-52-57.png

Let's say for example that I have an NFT that's closer to a character in a RPG. Strength, Dexterity, Constitution, Intelligence, Wisdom, and Charisma are a standard set of integer based character attributes. Let's try and represent these statistics 1-10 in the smallest space in JSON format:

...
"s":9,
"d":9,
"c":1,
"i":0,  //0-9 instead of 1-10 to keep the bytes to 1 each.
"w":8,
"h":5,
...

That's 36 bytes! Which as we discussed earlier is more than half of that space for a layered NFT! Not only that, but if you are tracking many more things here, you'll run out of single character names like we already did with "c" and cHarisma is now represented by an "h".

"s":"9,9,1,0,8,5",

Is one more way to track these statistics and best case will approach 2 bytes per statistic. You can go into base64 here as well and get granularity up to 0-64 into two bytes. Which depending on your use case may be the best way to save space.

However, in the definition portion of a layered NFT we can specify how to store these parameters in a much more intelligent way: via prime factors

primes = [
    2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 
    31, 37, 41, 43, 47, 53, 59, 61, 67, 71,
    73, 79, 83, 89, 97, 101, 103, 107, 109, 113,
    127, 131, 137, 139, 149, 151, 157, 163, 167, 173,
    179, 181, 191, 193, 197, 199, 211, 223, 227, 229,
    233, 239, 241, 251, 257, 263, 269, 271, 277, 281
]

These are the first 60 prime numbers, and any combination of multiplication of these numbers results in a unique number.
Now we are going to make the largest possible number from our character stats scheme and see how small it can get:
29 x 71 x 113 x 173 x 229 x 281 = 2,590,136,759,459 ( 903,957,729,051,191 with 7 maxed out)
Which is biGBdIZ in Base64. (3DYKFLG8t with 7)
Let's put this into our JSON format now:

...
"s":"biGBdIZ",
...

We went from 36 bytes to 14 bytes, and let's add those extra 7 stats we could be tracking:
"s":"biGBdIZ,3DYKFLG8t",
24 bytes for 13 categories, or 22 bytes for 12 categories which is under the limit for the above method. There is of course a bunch of working space in here, like for example if you only track some stats from 1 to 5... the extra stat from 6 to 7 added 2 bytes but adding a new part adds at least 2 bytes. You can also store up to 6 binary values/flags in one byte of base64. What works best for your NFTs will likely be a well thought out combination to your needs.

We do want to keep our working numbers to lower than 9,007,199,254,740,991 because using the BigInt library doesn't run in constant time and could make the chain susceptible to a timing attack. For example: Placing really large numbers that you've coded your nodes to ignore, but the standard code set doesn't ignore them and therefore the standard nodes miss their reports while your nodes take over consensus.

Let's code something to find our six stats:

const stats = Base64.toNumber('biGBdIZ')
var statArr = []
for(j = 0; j < 6; j++){
    for(i = 0; i < 10; i++){
    if(primes[i + (j * 10)] % stats == 0){
        statArr.push(i)
        break;
    }
    }
}
// statArr[9,9,9,9,9,9]

Playability

This might get into some bad news. Let's look at Splinterlands and how they play a game with their NFTs. First each card is built from other cards, there is really only a merge aspect that gets tracked. Meaning, how many cards are stacked into a card to raise it to a level. Cards can be levels 1-4 for rares and 1-10 for commons. Each card of any level will have the same stats as any other card of that level.

But you're building an RPG, and your character cards may earn Strength from a Pull-Up Quest, or Constitution from an Eating Contest. This means that each way to alter a cards stats have to be some kind of code that's at least somewhat distributed or trusted. An EVM (Ethereum Virtual Machine) might be a great way to put these code snipets into consensus, or maybe you have a game server standalone that is trusted by the decentralized network to append card modifications (NOT IDEAL).

Unfortunately, just having an NFT definition won't likely include the code that will run a game. Of course a standard game engine could be made, one that gets some standardized sets of attributes for NFTs, and this could be really cool especially if you want to be able to play NFTs across multiple games. Animal Crossing comes to mind here where you can personally do many things, like knit a pattern for clothes, and make that pattern portable as well as unique.

A solution likely to be employed is building a layer 2 just for your game. One where you can HardFork new quests and Gameplay onto the chain. Much like WoW you could earn native "gold" and pay rent on your "house" that includes the space for all your in game assets, This might be a monthly payment model, or if the economy is healthy enough... fees on auctions and other in game services to pay the layer 2 miners. Only NFTs that are stored in game can be played and modified... dismantling an NFT might give some standard items like rubies and emeralds that can be used to enchant weapons and armor. Banking an NFT might involve moving it to competing cold storage layer 2s that can't modify the item, but can hold it, and transfer it back to the parent chain that only tracks it's location and a checksum to minimize space and ensure it's not tampered with.

What's Next

  • Definitions and Minting
  • Auctions and (Trustless) Transfers
H2
H3
H4
3 columns
2 columns
1 column
12 Comments