EthDenver: Cryptogs — How Did It Start and How Does It Work?
Cryptogs smart contracts introduce an innovative pseudo-randomized player to player game on the Ethereum network using ERC721 tokens in which challenges of player to player trustless interactions, trustless pseudo-randomness, and transactions with multiple ERC721 tokens are addressed. Credits to Austin Griffith for smart contract design and continuing efforts on the front end, Patrick Mackay and Jonathan Gorczyca for additional work on our front end design. I contributed on the unit testing side and now translation of this design to a broader audience. Like breaking things? Check out the game and the bounty here. #BUIDL
How Did It Start?
Cryptogs is game hosted on Ethereum network that mirrors the traditional pogs game born out of the 1990’s. The idea was hatched on the way to EthDenver and has grown into an operational development in which you can actually play pogs against other players on the blockchain. You can find the game at https://cryptogs.io/. Here you’ll also find an open bounty with plenty of ETH to still hand out, so if you are interested in contributing, we encourage you to use this article as a starting point to understanding our design and get hacking.
As part of the Cryptogs team, the aim here is to take you through how the idea came about and how the contract works. If you looked at my past posts, you would surmise that I am not a traditional “gamer”, and you would be absolutely correct — save for past sibling rivalry over MarioKart. Stepping into EthDenver, however, it was exceeding important to be open to learning. As Twain said, “You can’t depend on your eyes when your imagination is out of focus.”
As I approached the Sports Castle, keeping my eye out for a team to join, Austin Griffith was driving in from Fort Collins drawing up ideas of what to build. Jonathan Gorczyca had arrived from New York, and Denver native Patrick Mackay made the short drive into the hackathon. We connected and drew up a handful of ideas: digital collectibles for endangered species to benefit organizations working to save them, industry certification verification through the use of Uport (unsurprisingly, yes, this one was from me), and what about a game? Austin brought up the game of pogs.
And what is pogs?
Pogs, or milk caps as it is known by some, is played directly player-to-player, one person against another. In reality, a single pog is a small cardboard disk with a design on one side of varying uniqueness. Check them out here.
How is this game played?
(1) To begin, each player selects an equal amount of pogs from their own pile. Let’s say for this example, each player decides to play 5 pogs. Recall that each pog has a varying uniqueness due to their designs, so the choice of pogs to play from each competitor is important — why would I want to play my extremely rare No Fear pog against your average smiley face pog? In theory, a player would not take the risk of losing their pogs to opposition unless there was a potential of gaining pogs of equal or greater value.
Challenges: How do we set up individual cryptogs of varying uniqueness and varying value? How do we allow players to select a game against another player on the blockchain without hazardously exposing their cryptogs?
(2) Once the stack of ten is selected (5 from each player), the players flip a quarter to determine who will go first.
Challenges: How do we introduce the most reliable, trust-less, most randomized solution for determining how the coin flip will work to see who goes first?
(3) The winner of the coin toss takes their slammer, which is essentially a heavier weight pog that is used as a game playing piece, to slam down on the stack of 10 pogs, which is stacked in alternating ownership (ex. player 1 pog on top of player 2 pog on top of player 1 pog on top of player 2 pog, etc). In throwing the slammer onto the stack of pogs, the goal is to “flip” the pogs as each pog that is flipped goes into the possession of the player who threw the slammer. In reality, the effectiveness of the flip is determined by the force and angle in which the slammer is thrown. Unless you are playing pogs against Isaac Newton, it’s likely that the effectiveness of your slammer throw is pretty random.
Challenges: Again, how do we introduce the most reliable, trust-less, most randomized solution for determining how coins will flip?
(4) Sometimes the whole stack will flip when a slammer is thrown, sometimes only one or two. If there is still un-flipped coins in the stack, then the alternate player is given a chance to throw their slammer on the stack to flip.
Challenges: How do we play the game out for ten pogs flips (5 from each player)? Do we flip each pog individually or how can we introduce randomness into how many pogs flip at a time?
(5) After all pogs have been flipped. Each player takes possession of the pogs that he or she won from the game.
Challenges: How do we handle the transfer of pogs from one player to another? And how do we prevent a malicious player from locking up another player’s tokens inside a game?
As you can see from the above, this was not solely a game of collectibles or solely a game of randomization, there was an interesting set of challenges to approach through smart contract design — and user experience.
Deep Dive Into the Cryptogs Contracts
How do we set up individual cryptogs of varying uniqueness and varying value?
Although the most prominent Ethereum token standard is the famous ERC20, the ERC721 token standard is encountering a similar rise in use cases. Unlike the ERC20, the ERC721 token standard allows for various tokens in the same set to hold different value. In the ERC20 standard, each token in a set holds the same value. For example, 1 GNT is equal to 1 other GNT. However, in the ERC721 standard, 1 token in a set can hold a different value than the others. For example, take cars — every 4Runner created by Toyota is identified as 4Runner, but there is a big price difference between an old 2004 SR5 4Runner and a brand new 2018 Limited 4Runner. The ERC721 tokens allow one to hold assets of the same class but unique value according to certain characteristics, and for that reason, we decided to represent a cryptog as an ERC721 token.
Cryptogs contract, you can see that when we mint cryptogs, the image information associated is stored as information in reference to the id of the cryptog created.
What happens above when we send a request to mint to the
Cryptogs contract is that a token
newID is generated, starting from 1 and increasing incrementally. With each id, we keep a reference to the image displayed on that cryptog in the
item struct holding the image data as a bytes32. (If you are a savy Solidity developer, you know that indexing for arrays and structs starts at 0; however, to avoid indexing complications further down the line in the contract, at contract creation, we establish an empty placeholder in our Items array of structs at the 0 index.) When minted, the cryptog is sent the
_owner address identified in the function call, meaning the new cryptog is sent to the identified owner.
That is the basic idea of creating a cryptog, but in our design, we have set it up such that each player must commit five cryptogs to a game to challenge another. Minting cryptogs one at a time would be exceeding tedious in this case, so we have included a function call so that ten cryptogs could be minted at any one time. In this way, we mint packs of ten cryptogs at a time. If you notice, these ten cryptogs are initially owned by no one as the
_mint() function does not transfer the cryptog to anyone but merely creates it in our
item array. In addition, each of these ten cryptogs produced is stored in a struct
pack with their associated ids and price. When purchasing via the
buyPack() function, a player selects the pack of pogs that they would like to purchase, and each individual pog is sent to them from the pack.
You’ll also notice at this point that we have restricted the minting operations to the cryptog team (
onlyOwner). Why? Well, we established that a cryptog would potentially hold value by its uniqueness. Imagine if you held a rare EthDenver Bufficorn, and another malicious player decided to mint their own Bufficorn coin — 100 times more than existed in the game! Well, unfortunately, even though your Bufficorn cryptog may possess unique bytes32 image data, that would be a difficult assessment for an non-blockchain average user to make in the UI — and a copied image can look completely identical, perhaps the copies would able to disrupt the game in the same way that offbrand items take market share away from name brands? Plus, if this sinister minting continued, the game could be continually upended — and we’d have a mess of ERC721’s. Perchance in the future, it is possible to open up minting coins to other users through some other logic, but that is one piece we have set aside for now to complete the project.
How do we allow players to select a game against another player on the blockchain without hazardously exposing their valuable cryptogs?
After designing the contract such that we could balance out the minting of unique cryptogs, we had to ensure that players could challenge each other to a game of cryptogs on the Ethereum blockchain without hazardously exposing their cryptogs. To overcome this challenge, we wanted to ensure a few qualifications were met:
- Any player (Player 1) could set up a game of cryptogs .
- Any player (Player 2) could challenge another (Player 1) to their proposed game of cryptogs.
- Player 1 could accept or reject Player 2’s challenge.
- Player 1 and Player 2 could cancel their proposed game at any time prior to the start of the game.
We accomplish the ideal that any player could set up a game of cryptogs through the
submitStack() function below. Any player can choose five of their respective cryptogs to play. The contract checks that (1) the
slammerTime contract is set — which I will detail more in a minute, (2) that the player submitting the transaction owns the tokens that they choose to submit, and (3) approves the
slammerTime contract to be able to move those respective tokens. Note that at this time, the tokens are not taken from the ownership of the player submitting a potential game. Until a game is confirmed, players hold onto their individual tokens. You’ll also notice that an additional
bool is included asking the player whether or not they would like to keep their game as public (true) or private (false). A public game is broadcast to everyone on the front end, while a “private” game, would not be broadcast on the front end. Although a “private” game would still be available to see on the Ethereum blockchain, we could still hide certain games from the front end that a particular player does not want to exposure in the game. At present, all games are broadcast as public to facilitate participation, but in the future, we can play around with this functionality.
Lastly, you’ll see that this stack submission is given an individual identifier through the use of the a mapping of the hash of a nonce and player 1’s address (
stack) to the
stacks stuct that keeps a log of the ongoing cryptogs games, which includes the address of Player 1, and the block number of the specific challenge. Why the nonce and the address and the hash? To ensure that the hash of one player’s game cannot be generated by any other player and rewritten in the struct.
Next, we want another player to be able to challenge Player 1 who submitted the stack above. In other to do this, we allow any Player 2 to submit a challenge stack against Player 1’s unique stack through the
submitCounterStack() function. Player 2 chooses their cryptogs that they would like to play with as well as identifies the unique stack from Player 1 that they would like to challenge. Again, we check that (1) the
slammerTime contract was set — the explanation is coming I promise, (2) the cryptogs identified to play with actually belong to Player 2, (3) approve the
slammerTime contract to move all the cryptogs, and (4) check that the player is not submitting a counter stack to a game that they proposed themselves.
At this point, we also include the counter stack from Player 2 in our struct of
stacks with our unique identifier created from the hash of Player 2’s address and the nonce. Further, we map this counter stack identifier (
counterstack) as a challenge to Player 1’s original stack through the use of
stackCounter — we are not “counting” a stack with this variable but mapping the hash of Player 1’s stack to the hash of Player 2’s challenge stack.
Again, at this point no cryptog transfers of ownership have taken place — just proposals of games and the authorization of the
slammerTime contract to be able to transfer those cryptogs proposed.
Next, we mentioned that we would like for Player 1 to be able to accept or reject Player 2’s challenge. This is accomplished through the
acceptCounterStack() function. Player 1 identifies their unique stack and the counter stack that they would like to accept. At this point, we check that message sender is the owner of the Player 1
_stack that they are proposing, and we check that the
_counterStack that they identified coming from Player 2 is actually challenging them by checking that it is mapped as a challenge to
_stack. You’ll see that we require the
mode, which is mapped to Player 1’s stack identifier is set at 0.
Mode is a parameter that increments throughout the game play, so that reentrance is prevented, and Player 1 and Player 2 will not try to overwrite any plays by submitting a transaction for the same step in a game twice. Next, we finally have the transfer of all cryptogs to the
SlammerTime contract. This contract holds the cryptogs in escrow until Player 1 or Player 2 wins the cryptogs in the game. Note that in this time, should another malicious Player 3 attempt to submit a counter stack in order to dupe our Player 1, since
acceptCounterStack() requires Player 1 to identify Player 2’s stack through Player 2’s respective stack identifier, Player 1 could not accept a malicious stack (unless Player 1 accidentally sent in Player 3’s counter stack id instead of Player 2’s). However, in this case, if Player 3 had managed to have their
submitCounterStack() transaction mined prior to Player 1’s planned
acceptCounterStack() against Player 2, then Player 2 would need to submit their counter stack again to Player 1 as the
stackCounter mapping would be overwritten by Player 3. This scenario would be peculiar because other than the satisfaction of disrupting the game, malicious Player 3 would derive no financial benefit, and it would actually cost him or her gas. For that reason, we have not expanded a further work around for this situation.
Additionally, at this point we log the (1)
lastBlock number of the game, (2) the
lastActor in the previous transaction (Player 2), (3) set the mode of the game to 1, and (4) officially set the
counterOfStack mapping Player 1’s stack to Player 2’s to confirm that Player 1’s stack is accepting the challenge of Player 2.
Lastly, we map Player 1’s unique stack identifier to an array of the ten cryptogs to be played in the game (
mixedStack) “stacked” in an alternating order (player 1, player 2, player 1, player 2, etc.).
Now, we also mentioned that prior to the start of the game Player 1 and Player 2 should be able to withdraw their respective stacks if they so choose. In the below
cancelStack(), Player 1 may cancel their stack so long as the
mode is still zero and
stackCounter has not been mapped, meaning they are not trying to cancel a counter stack — recall that a counter stack can be rewritten by whichever Player 2 submits the next stack counter stack, so there is not a need to cancel it per se for Player 1 to accept their desired counter stack. In
cancelCounterStack(), Player 2 may cancel their counter stack so long as the game has not started as well (
mode is zero), given that Player 2 correctly identifies their owner counter stack and Player 1’s stack which they challenged.
Whew, okay. We have finally started our game! Now, on to the coin toss.
How do we introduce the most reliable, trust-less, most randomized solution for determining how the coin flip will work to see who goes first?
As mentioned above, Player 1 will “flip a coin” to determine whether Player 1 or Player 2 throws their respective slammer first. We need to ensure that there is no way that Player 1 may be able to game the system by forcing the coin flip to be in their favor. We accomplish this to the best of our ability through a commit-reveal scheme, which requires two transactions in which Player 1 (1) commits a random hash to the Ethereum blockchain and later (2) the original data revealed and used in conjunction with the block hash to generate a random hash. For our particular case, if the random hash is even, then Player 1 goes first, and if the random hash is odd, Player 2 goes first.
Let’s see how this works. First in
startCoinFlip(), you can see that we verify Player 1 as the message sender, and that the correct stack and counter stack are referenced with the
mode being set to 1, meaning the game has started with Player 1’s acceptance of the game. A
commit hash, which is generated by the front end, is also mapped to the respective
_stack being played. We generate this hash through the front end at this point — without Player 1 seeing or selecting their respective hash. As far as user experience, we judged this to be the simplest way for new comers to experiment with the game and avoid additional, potentially confusing, steps to play the game. You will see in the next transaction that even if other players were able to find out Player 1’s submitted original data and committed hash, this would not have a bearing on the outcome of the coin toss, even though the former data is available through an investigation of our front end design, and the latter is available on the Ethereum blockchain. Lastly, we save the block number in which this transaction was mined for reference in
After completing the coin flip start, Player 1 is able to end the coin flip through the
endCoinFlip() transaction. Here again we check that Player 1 is sending this transaction, and Player 1 is referencing the their correct stack and counter stack. We check that the
mode is set to 2, meaning the coin flip has started. Lastly, we require that the current block number is higher than that of the
commit block from the previous
To determine who won the coin flip, we check that the hash of the
_reveal matches that of the commit that was previously sent in
startCoinFlip(). If Player 1 sends in the wrong
_reveal, the coin toss fails, and must start over again. For our purposes, since the commit/reveal scheme is handled on the front end without the user seeing this transaction, the chances of sending in the wrong
_reveal are low unless one attempts to mess with the front end configuration. However, should Player 1 intentionally mess with the front end and send a false
_reveal, then they must restart the commit/reveal scheme to alleviate the assumption that they may have guessed incorrectly while attempting to manipulate the outcome of the coin toss.
Finally, if the
_reveal is correctly received, then the
mode of the game incremented to 3, and we start to keep track of the
round of each game to induce more randomization into the coin flipping, which is coming up next. Then, a
pseudoRandomHash is generated using the
_reveal and the blockhash of the
commitBlock in which the
startCoinFlip() transaction was sent. In this way, if Player 1 wanted to mess with the outcome, then Player 1 would need to be the miner of the
commitBlock in such a way that the blockhash would be advantageous to the commit they submitted, and they would need to ensure that the blockchain followed their mined block to this next
endCoinFlip() transaction block. Although at some point a quantum computer may make that look easy one day, for now, it is a very, very hard play.
Next, we take the uint256 representation of the
pseudoRandomHash, and if that number is even, then Player 1 goes first. If the number is odd, Player 2 goes first. The
lastBlock of the game is saved to begin a timeout period of 180 blocks (~forty five minutes), so that Player 1 or Player 2 does not lock up one another’s coins indefinitely should they decide not to complete the game. The
lastActor is recorded as the opposite to which player won the coin toss — more on this coming up.
Now, we have flipped our coin to see which player will be the first to “throw their slammer” on the stack of cryptogs in order to make them “flip.”
Again, how do we introduce the most reliable, trust-less, most randomized solution for determining how the cryptogs will flip?
We are familiar with the commit/reveal scheme at this point, and hence, it is not surprising that we again use a commit/reveal scheme to induce some randomness into how the cryptogs will flip when a slammer is thrown.
raiseSlammer(), the player who won the coin flip, who is the opposite of our
lastActor, is the player that will be approved for submitting this transaction. We check that the player is submitting the correct stack and counter stack, and require that the
mode is set correctly in the sequence of the game. We save this
commit, and increment the
mode to the next sequence in the game.
You have made it this far, and it is time throw the slammer and flip some cryptogs! You see below in
throwSlammer() we again check the
_reveal to generate a
pseudoRandomHash. In the interest of preserving words, I’ll let you refer back to the explanation of how this commit/reveal works in our
How do we play the game out for ten pogs flips (5 from each player)? Do we flip each pog individually or how can we introduce randomness into how many pogs flip at a time?
The interesting part here is determining how the cryptogs flip. Let’s start at Line 16 below. After we have validated our
_reveal, we initialize an empty array of 10 ids to store our flipped coins. In the case of an incorrect
_reveal submission to the
throwSlammer() event, we set the
mode back to 3, which requires the player to commit again at the
raiseSlammer() transaction. Verifying that the correct
_reveal was submitted in the transaction, we save the
previousLastActor in the game to show on the front end who will go next in the
throwSlammerEvent() function (see line 50), and again we generate our
pseudoRandomHash as well as record our
lastBlock and the
lastActor of the game.
At Line 33, we start the flipping randomness by setting up a
done variable as true. In this way, we are able to record whether the cryptog flipping is completely done for all ten cryptogs or just this round is completely flipped, meaning there are still cryptogs — remember a slammer may make all cryptogs flip or only a few. We start a
randIndex to increase the randomness with each iteration through the stack of cryptogs as this will be used to index our hash for a number. Next, we iterate through the whole stack of cryptogs. For each cryptog, we generate a psuedo-random uint8 through taking an index of our
randIndex. We then compare this number,
thisFlipper, to another uint8 generated by multiplying a increment index of the original
_stack hash identifier submitted by Player 1 and multiply that by our
FLIPPINESSROUNDBONUS variable and adding that to our
FLIPPINESS variable. Just randomness.
How do we handle the transfer of pogs from one player to another?
We compare to see if
thisFlipper is less than this alternate pseudo-random number generation (Line 38). If it is, we transfer that cryptog to the player who threw the slammer, meaning the cryptog is moved through the
SlammerTime contract to the respective player and removed from the
mixedStack array. In the case in which a cryptog does not meet the criteria to be flipped,
done is set equal to false — indicating that there are still cryptogs to be flipped and another round is required so below in Line 66, our round variable is incremented. If all cryptogs in a particular round are flipped, then the
done variable remains set as true. Consequently, at Line 52, we ensure that we end our game by deleting all records of our game tracking variables.
And how do we prevent a malicious player from locking up another player’s tokens inside a game?
If you recall from throughout the game, we are updating a
lastBlock variable in which the block of the last transaction is recorded. Let’s say Player 1 somehow sees that they are going to lose the coin toss and decides not to reveal their commit. For this case, we have included a
drainStack() function in the case that if any player refuses to continue with the game transactions for more than 180 blocks (~45 minutes), then the opposing player may execute the
drainStack() function to retrieve all the cryptogs. Below you can see in this function that we verify the sender is of the same game and that the sender is the
lastActor in the game, meaning this player would have been be waiting on the opposing player to submit their transaction. We do not let the opposing player that did not submit their transaction drain the cryptogs. Since all remaining cryptogs would go to the opposing player, we incentivize players to not lock up any cryptogs due to refusal to submit transactions, which means any player cannot retrieve their cryptogs that they previously committed to the game without either flipping them or experiencing the case of a timeout from opposing player.
If you have made it this far, thanks for reading! You hopefully have an excellent understanding of how Cryptogs works and how it was built. You can further examine the contracts Cryptogs and SlammerTime as well.
EthDenver challenged the community to deliver on Technicality, Creativity, Usefulness, Design and Making the World a Better Place. Our aim was to deliver on each of those points — if not during the hackathon, then certianly after. The Cryptogs team aims to continue refine our design and share our work with the Ethereum community. See Austin’s analysis of the randomization challenge here along with other ideas for improvements. And lastly, in terms of making the world a better place, why not let non-profit organizations mint their own collectable cryptogs for raising money? Gaming for non-profit benefit is alive and well already, and we would like to build a model around that as well.
A huge thank you to all organizers and sponsors of EthDenver. #BUIDL
See you around Cryptogs!
— The Cryptogs Team