Building Trust in Gods Unchained

Are you hesitant about spending money on digital card games? Do you find card less trustworthy than physical cards? In this article I’ll try to argue that it should in fact be the other case around

I’ll talk about the importance of transparency, authenticity and I’ll go through how Gods Unchained achieve a high degree of randomness in the card packs you buy. All relevant for building trust and both are achieved by using blockchain technology. There will be a few code examples, and some blockchain tech-talk, but it shouldn’t require much prior knowledge to follow .

For anyone who stumbled across this article and are not familiar with Gods Unchained: It’s an online collectible card game with tradable cards and ownership guaranteed by blockchain technology. It’s close to Hearthstone in gameplay, but in my opinion it contains a lot of subtle depth that makes it tempting to compare it to Magic the Gathering as well, which I’ve been a fan of for more than 20 years.

Previously I’ve written an article about the different card pack types you can buy, which you can use to maximize value despite the randomness of the process:

I’m not affiliated with Gods Unchained, nor have I been contracted to write an article. I have bought cards so I do have an interest in this game. This is my card collection. For the record, I’ve also developed gucollection.com which I use as a resource in my articles.

An Issue of Trust

There are three aspect I want to talk about regarding trust for an asset based game, such as Gods Unchained:

  • Transparency in distribution of assets
  • Authenticity of assets
  • Randomness in distribution of assets
Playing cards are digital assets in Gods Unchained

In the preface I mentioned ‘high degree of randomness’, which might sound like it isn’t truly random. Well, I would argue that nothing is. It’s all a set of variables, and if you can control the variables, you control the outcome, or at the very least, tilt the chances in your favor depending on which variables you control. By high degree of randomness I mean that none have an visible advantage over others.

So what is random? A coin toss is often cited as a simple explanation of something that demonstrate probability and randomness. It’s often used in sports, and even has its appearances in politics: Hilary Clinton won the Iowa Caucaus off the back of a coin toss. It’s seemingly a 50/50 random change of either heads or tails. But it too is a set of variables that can be controlled. Catching the coin by the hand for example gives roughly 3 major variables:

  • The coin itself, which is easy to control. Coins are generally not perfectly weighted, and could be modified to increase your chances even more.
  • The force of the flick and thirdly, the landing. These two can be practiced (or just use sleight of hand), and it wouldn’t be a problem tilting the chances in your favor.

If you let the coin fall on a hard ground however, it becomes a different story. The amount of variables are increased, making it much harder to predict, though the properties of the coin still can affect the outcome. If anyone are doing a coin toss and catching it by the hand you are basically trusting the people, not the process.

I also mentioned Magic The Gathering in the preface. The company behind it, Wizards of the Coast (WotC), actually don’t aim for a high degree of randomness with their physical card distribution. The cards in the packs you open are not actually randomly selected! Every card you can get is printed on sheets according to the fixed distribution of common, uncommon, rare and mythic (and foils). Next the cards are cut and packed. But they pack it according to algorithms in booster boxes (36 booster packs packed and sold together). This meant that for awhile ‘box-mapping’ was possible: By sampling a few of the packs, the rest of the cards could be predicted.

So why don’t they print the cards in an random order before packing? That wouldn’t be that hard to do. Well, I don’t think they want it to be random. You might end up with 15 of the same common card in one booster box, while you get zero of another common card. Or no mythic in one unlucky booster box and 10 in another. That’s just an inherent trait of random: anything is possible, and with enough attempts, it will happen (it’s just not probable).

What WotC do aim for is a good distribution of cards, while minimize the risk of being able to predict what cards you get. When I’ve opened booster boxes I always end up with all common cards. I might only get 2 of a few common cards, but 4 of most, and usually not more than 6. Uncommon and rarer cards are pretty much distributed according to what they promise (mythic foils is of course another story). The upside on this process is an even distribution where people get what the expect to get, along with a few bonuses from time to time. The downside is that you have a process that works for most people, but is vulnerable for the few people who are able to exploit it.

Nowadays WotC have added more variables to the process, so it’s probably impractical to do. It’s still not random, it’s just that for most buyers there are too many hidden variables. I say most, because I don’t know if there are someone who are in a position to exploit it. And that’s an important point. Auditing the process is not possible, which is what transparency is all about.

There is another disadvantage with the whole way physical cards are distributed. The buyers do not know if something is happening behind the scenes: They could be giving away rare foils for a selected people, or printing more than promised, or they screw up their packing and shipping and skew the distribution. How do we know how many of any one card exists, and how they came into circulation?

I’m not saying anything unfair happens, but the process is not transparent, so the general public can’t know for sure.

A physical card is easier to trust than a digital one for many. After all, everything digital has been as something which is easy to copy, steal or otherwise manipulate. Or you don’t trust the company to store your digital copy on their servers.

In MTG it is the vintage and legacy cards that were prone to counterfeiting, since their scarcity and price made it profitable to create good copies. For newer cards countermeasures like holograms were added, to make it harder to copy, much in the same way central banks have tried to combat counterfeit money. The issue is that these newer cards are not so new anymore, and tournament demand is driving the prices up, making also those profitable to counterfeit. When it reaches the point were people can’t tell the difference, you are basically printing money and creating inflation.

Here is a good article about the issue: https://www.mtggoldfish.com/articles/the-fake-card-problem

Still, holding a physical card feels easier to trust in the same way that physical money feels more secure. But that is starting to change for both, and the roles are being reversed.

Enter Ethereum

So how can a process you trust be built? With MTG, as with the coin toss, you are trusting the people involved (WotC), not the process. How I think it should be:

  • The probabilities for getting cards are the same for everyone (random)
  • All cards in a set are distributed in the same way (transparent)
  • It’s not possible to counterfeit cards (authentic)

In the next section I’ll go into detail how randomness is achieved in Gods Unchained, but I’ll cover the two other points in this section.

Everything on the blockchain exists on a public ledger that is available on a lot of servers (nodes). Using exploring tools like etherscan.io you can verify the state on the chain. For example you can look at the smart contracts of Gods Unchained, such as the buy rare pack contract. The code is available to see, and you can see what probabilities are implemented (as I did in this article).

I’ll show how transparency works when you purchase card packs: When you buy one or more packs an ID is returned after the purchase is complete, and all this is stored on the blockchain:

The event for the purchase on the public blockchain is shown here. The purchase ID is circled in red. The second hex value is the account. This is from etherscan.io

If you know the purchase ID, you can even see what cards they got in that purchase (click on the ‘score’ cell, 30.8, to see all cards).

Off-chain API (i.e. not using the blockchain to retrieve info) are used by gucollection.com for convenience to actually look up these cards, you can actually verify that the API is correct, as I will demonstrate later when we look at how randomness is achieved. And that is the whole point. If something can be verified, it’s not a problem using off-chain functionality, because they are forced to be correct, since the process can be audited.

It’s also possible to look at how many copies there are of one card. Use mouse over the name to see #owned/#exists (the different colors shown beside the picture are for different ‘shines’, while the number in black in the totals). If you need to see more information about the card, click on the # cell.

Digging deeper, you can see which purchases copies of that card appeared in for that account. Click on the ‘score’ cells to see all cards and the purchase ID. With the purchase ID you can backtrack to the transaction it appeared in, like in the image above — and verify that it was processed by the smart contract (there are some special cases were this can be tricky, but the info is there). This is important, because you don’t don’t want there to be cards from a set that is sold that didn’t originate from a purchase.

If information from all accounts are retrieved in this way it’s even possible to look at one card, and see which purchases all copies originated from. Then you’ll know that there all copies have been through the random function — making it an equal playing field for all — and transparent for all to see. A lot of information must be processed to do this, but it’s possible, and again, that’s the point. The process can be audited.

Above I explained that it’s possible to see the origin of each card. This is good for transparency and it makes it possible verify that someone is not inflating a valuable card with cheap counterfeits. More often though, the question about authenticism comes into play when you are buying a card: You want to make sure that the product you buy is worth it and legal to play with. If you can’t do that, that lowers your incentive to buy, and actually debases the product. I’ll talk how you can verify the authenticity of a card, but first I’ll talk about Activation.

Any card that is sold (hence all card you buy) have to be ‘activated’ first. That’s the process of creating a non-fungible token (NFT) of your card (it follows the ERC-721 standard for those interested). The cards you buy are tied to your account in the smart contract through the purchase, and you can play with it in-game, but it doesn’t actually exist on the blockchain on your account unless it’s activated. Owning a card as a NFT means that you have a verifiable unique copy of a card, and there is nothing anyone can do about it. Even if the company producing the game went out of business and removed the game servers, the NFT ownership means that someone could re-create the game, and people owning the cards could continue to play (it’s an extreme situation that isn’t likely to occur , but still it’s possible). More importantly, the stats of that card is locked on the blockchain. Properties such as mana cost, power and toughness cannot be altered. If the company wanted to change how the card played, they would have to implement that change in-game, but doing so would would seriously damage usability and the trust of the game.

Activation is done through a smart contract, after which it gets a token ID — which is an unique ID for a card. So if you for example want to buy a copy of ‘Gleeful Pillager’, you can use the token ID , and view it on the blockchain. The first transaction for that card will be the activation transaction. Looking at the ‘input data’ of that you can see that the purchase ID is 48149, if you press the ‘decode’ button (there ethereum address of that account is also available in the transaction). Using this information we can now look at the packs in that purchase to find the card. This example is pretty interesting, because there were 4 plain ‘Gleeful Pillager’ cards in that purchase, so which is which? Now, you could look at the purity number, because it’s a pretty low chance that they have the same purity (a number from 0 to 3999 that determines shine— in this case it was 37), but that’s not unique. The best way is to look at the sequence they appeared in, because the activation happens in sequence, and this was in the same pack as a Shadow ‘Sleep Dart’ (token ID 365532), so then we know. The activation process will be simplified before the game launches, but it should be possible to track the cards in a similar way also in a future version.

For buying purchases it should be enough that you can verify that it came from a Gods Unchained pack, you don’t necessarily care which pack it was. You might care about the purity number, but as long as the activation and purchase happened as it should, it will be the authentic one. No paper cards (or paper money for that matter) have this level of publicly available information about authenticity.

The verification process might not look straightforward, but everything I’ve done here could be automated. Another benefit of this process, is that if any card becomes reliably reported as stolen, it could even be marked at auction houses as such, and blacklisted in the game.

Random Functions in Ethereum

In this section, I’ll cover the third aspect, randomness. Blockchain-technology does not excel at random, but the again, computers in general don’t. However there are some mechanisms available that can gives a high degree of randomness if used correctly. I’ll explain in detail how that is achieved in Gods Unchained, with snippets of actual code from their smart contract.

First I’ll go through the purchase process as that’s key to achieving randomness. I’ll use gucollection.com as an example, though it’s more or less the same as the official one:

First you choose product and press the buy button. This triggers Metamask to ask for approval for sending the required amount. The second step is because sites are generally not authorized to sign transactions on your behalf, after that the purchase process initiates:

  1. Payment is sent: The amount was accepted in Metamask (or similar tool). You get a unique transaction id which is pending confirmation by the Ethereum blockchain. This can take a few seconds, depending on the speed you chose (the faster, the more expensive) and blockchain traffic.
  2. Transaction is confirmed: Your account is charged with the amount. The purchase is registered in the GU smart contract with a Purchase ID. A “PackPurchased” event is sent out by the smart contract, containing the purchase details — but not the cards just yet.
  3. Randomness is generated: A “RandomnessGenerated” event is sent out by the smart contract. That step is initiated by a automated process, but it can be initiated by anyone manually — which could sometimes speed things up. I’ll explain later why this is a separate step from step 2.
  4. Purchase Complete: Packs are ready to be opened. Actually, after step 4, the cards are already determined, but the services and official APIs need a small amount of time to process the information, and in order to open the packs you need to wait a little bit.

Now that we know about the purchase process we will look at some solidity code from the official smart contract (I’ll simplified the code a little bit, and will walk you through the functionality):

function getRandomCard(Rarity rarity, uint16 random)
returns (uint16)
{
if (rarity == Common) {
return common[random % common.length];
} else if (rarity == Rare) {
return rare[random % rare.length];
} else if (rarity == Epic) {
return epic[random % epic.length];
} else if (rarity == Legendary) {
return legendary[random % legendary.length];
...

Function getRandomCard provides the functionality of retrieving a single card ID, for example ‘Gleeful Pillager’ used as example above has 85 as ID (click on # to see ID under details of that card), so that’s the identity of the card (not to be confused with the unique token ID of a copy of that card).

The function checks the rarity, which is either common, rare, epic or legendary (I’m ignoring mythics in my example), and uses the value ‘random’ to decide which card ID within a rarity category will be created.

Next step is to see where rarity and random comes from (I’ve renamed two variables to make it easier to follow):

function getComponents(uint16 i, uint8 j, uint rand)
returns (uint randomness, uint32 rarity,
uint16 purityOne, uint16 purityTwo, uint16 random)
{
randomness = uint(keccak256(abi.encodePacked(i, rand, j)));
rarity = uint32(extract(randomness, 4, 10) % 1000000);
purityOne = uint16(extract(randomness, 2, 4) % 1000);
purityTwo = uint16(extract(randomness, 2, 6) % 1000);
random= uint16(extract(randomness, 2, 8) % (2**16–1));
return (randomness, rarity, purityOne, purityTwo, random);
}

Function getComponents provides the functionality of generating the random numbers (ignore ‘randomness’ for now). I’ve talked about rarity and shine in my previous article:

  • Rarity: Common, rare, epic or legendary — as used by getRandomCard
  • PurityOne: Plain, shadow, gold, diamond
  • PurityTwo: The purity number within each shine category, from 0–999. PurityOne + PurityTwo = Purity (both purity 45 and 995 are of plain purity, while 1450 is shadow for example — this number have little or no value).
  • Random: This is the random value that together with rarity determines card ID in getRandomCard above

All those random numbers decide the two important properties of a card: card ID (which card) and purity (how shiny it is).

The four random numbers listed in getComponents are each derived from a unique value: ‘ randomness’ (it’s confusingly called random in the solidity code of that method — but I renamed it here):

randomness = uint(keccak256(abi.encodePacked(i, rand, j)));

‘Randomness’ is a value made by the following method available in Solidity:

uint(keccak256(abi.encodePacked(<insert some value here>))

You can read this article to read more about the details. Suffice to say, abi.encodePacked converts your values into bytes, from for example numbers or text. This is necessary for the next method: keccak256. It is a hashing function, which takes arrays of bytes and creates a hash from it, a pseudorandom value, and is at the core of Ethereum cryptography. It produces the same output if it gets the same input, but you (generally) can’t go the other way (find the input from the output). The important factor however, is that if you make a minimum amount of change to the input, the whole output changes, i.e. can affect all the random numbers shown above. You can try it for yourself. This fact means that it’s often used as a way of achieving a high degree of randomness (unpredictable results). Because of the way it it’s used it’s critical that the value are evenly distributed, and for those interested they can read this paper on that topic, but it’s generally accepted that it’s possible to generate random values this way:

“The experimental results show the KECCAK hash function has excellent pseudo randomness”

—A. Gholipour and S. Mirzakuchaki

Accepting that we have a randomness value we will now look at the random factors, the variables:

randomness = uint(keccak256(abi.encodePacked(i, rand, j)));

So in order to create one card, there are three variables that decide it:

Pack index: The value ‘i’ is the index of the pack in the purchase. If you bought 5 packs this will be 0 for the cards in the first pack, 1 for the cards in the second pack, and so on.

Card index: The value ‘j’ is the index of the cards in a pack. It will be 0 for the first card in a pack, 1 for the second card, and so on.

The value ‘rand’: If it were only for the first two variables, every time you opened a pack you would get the same results. This prevents it and is actually the ‘randomness’ we talk about in the step ‘Randomness Generated’. It is actually a composite of several values:

uint random = uint(keccak256(abi.encodePacked(bhash, p.user, address(this), p.count)));

As we can see this is another hashed value with 4 more variables:

  • bhash is the hashed value of the block the purchase was done in
  • p.user is the account the packs was bought for
  • p.count is the number of packs you bought in the purchase
  • address(this) is the unique address of the smart contract

So in total it’s 6 values that decide which cards you get.

‘rand/random’ is not created on purchase, because one of the variables is missing upon purchase: ‘bhash’. That’s not so strange, since it is the hashed value of the block the purchase transaction was confirmed in. For those that are not familiar with the concept, a block is a list of transactions that are processed together and are confirmed at the same time. When it is completed the one who created the block also determines a hashed value. This is the critical factor of the process, because as I said early in the article, if someone is able to control the variables it stops being random. And the function keccak256 showed us that you need to control ALL the variables, since a minimum of change creates a complete new set of random numbers used for card ID and purity. All the variables are easy to control, except ‘bhash’. If you buy the same number of packs for an account through a smart contract you will get the same cards each time, if it were not for the block hash. If would be easy to write a program that used different numbers of p.count (packs in a purchase) and reported back which number would get you that legendary diamond you want.

The block hash is the true random variable in the process, since that is the one variable you can’t control. This is also why purchase and randomness are two steps. We don’t know the block hash until the block has been processed, so first you are forced to make a purchase, and then after you have spent your money, the randomness is generated.

Only the last 256 block hashes are available, so if the callback fails and we don’t have the block hash, there is a recommit that can be called in order to get a new block hash. Then callback can be called again. This guarantees that eventually randomness is generated.

There are several safe guards in place to prevent exploitation:

  • Packs can’t be opened until you have a random number. There was a case where block hash was used in a similar way for betting, but it didn’t account for the 256 block limit, so all you had to do was to wait 256 blocks and use a block hash of 0 to predict the outcome.
  • Callbacks are done automatically by a bot, and can be called by anyone. This prevents people who want to control the callback and recommit mechanism.
  • Callback fails if it’s the same block as the purchase. This guarantees that the purchase is confirmed, before you generate the cards, and stops people from trying to predict which cards they’ll get before they make a purchase.
  • Callback fails if it’s more than 256 blocks since the purchase. A recommit is then needed. This overcomes the limitation that there are only 256 block hashes available.
  • Recommit can only be called if it’s more than 256 blocks since the purchase. This ensures that no-one is trying to create a new block hash every block in order to maximize randomness.
  • Recommit can only be called if there were no callback made during those 256 blocks. This ensures that if the randomness is generated, another will not overwrite it. There are no second chances.

The randomness number is also stored on the blockchain (you can view it in the event generated as well). Using that value it is possible to verify that the cards you have received are the cards you got. As you remember from before, that was used to generate card ID and purity. This brings me to the next point: I mentioned earlier that it was not a problem using APIs instead of blockchain information directly. This is the reason. Using that random number means that it’s possible to verify that the values returned from the official API are correct. Most of the cards are off-chain, and this is likely to be the case, since not every cards needs to be traded, but the ability to verify both cards on- and off-chain (verify that purchase records exists on chain) is extremely important. Being able to do a public audit like that is very powerful because it builds trust in the game.

So is the callback (or recommit) exploitable? Possibly, but it would be really, really tricky, and I can’t see that it would be worth it. It’s similar with Ethereum. It’s possible to hack it, but the resources required for that makes it improbable.

The obvious one requires that you would have to be able to simulate buying after you knew the block hash, but before you made the purchase, before the block was confirmed.

Creating a simulation with all the variables is the easy part. There is one value that is easy to control: number of packs bought. It is possible to simulate the results and return if there were any favorable results. There are other values to tweak as well, such as account address.

The hard part is knowing the block hash before you make the purchase. In practice you would have to be the miner of that block. It would be improbable and not economical — if you simulated your way to a legendary diamond you would not be guaranteed to be the one who mined the block. This would be difficult.

It could also be possible to use recommit to give yourself more than one chance. The exploit then is to do a simulation with the block hash of the purchase. If you are unhappy, do a recommit and try again. When you are happy, call callback to store the randomness. But you would have to wait for a while between tries (because of the 256 block limit), and you are actually doing purchases (i.e. spending money) before you know the outcome. This scheme would also fail immediately if the automated process/bot (or anyone else) used the callback on your purchase ID. Those that control the bot could do this, but there would be a record on the blockchain of how randomness was generated (recommits are visible), so a systematic exploitation would eventually be discovered.

A small disclaimer: The risk might change with Proof of Stake (PoS), which changes the validation process, but new versions of the smart contract could be released to handle those issues.

If you are interested to read more about predicting random numbers you can read this article.

Conclusion

I’ve argued in this article that Gods Unchained achieve transparency of card creation and distribution, easy verification of authenticity of cards and a high degree of randomness, all using Ethereum blockchain technology. This helps build trust in a digital world, among others for tradable/collectable card games, in a way that would be hard pressed to match for paper cards.

There is a downside to randomness, which I will mention at the end. You will not have an even distribution of cards. After you’ve bought a thousand cards from one set, you will probably have at least 15 of one card while still missing a common card. That’s the nature of random. MTG reduces this by not packing cards purely in a random fashion. The downside however is mitigated by the fact that trading digital copies is much easier, and should be cheaper and more secure (exchange is secured by smart contracts). You can also use your spare copies to create higher shine cards if you want (5 plain equals a shadow).

I hope you enjoyed the article, and for those that have a Medium account, feel free to clap one or or more times.

Gamer, Engineer, Tech-curious