Two years ago, my colleagues participated in a weekend hackathon and came up with an idea to create a Rock-Paper-Scissors game for Facebook messenger. The real-world RPS game is simple and pretty much everyone knows how to play it. The mobile app didn’t aim to add cool new ways to play the game and tried to be as simple as possible.
Within the mobile app, the user selects either Rock, Paper or Scissors and sends the challenge to another person via Facebook messenger. The other person clicks on the received challenge in their private chat, selects one of the symbols too and a winner is subsequently announced. Plain and simple.
Since then, thousands of people installed the game and the game received fairly positive reviews too.
A few months ago, we applied for and eventually received a grant from KIN foundation to integrate their SDK into our application. Kin is a new cryptocurrency created by the same company that created Kik (a widely-used messenger in Canada).
My task was to add a betting mechanism to the matches. The challenging player chooses and pays a bet with Kin currency, the responder then also pays the same amount and the winner claims it all (or in a case of a draw, players just receive their bets back).
It was time to dust the project.
Before implementing any new functionality, I wanted to get more familiar with the project and understand its internal structure. But mere moments after opening the project I already started groaning.
The code was full of copy-pasted chunks of code. There was even a duplicated image-generating code that had about 70 lines of code. One of the copies had a two-line optimization, the other didn’t. Why? I don’t know. Probably the time constraints of the hackathon. Furthermore, the app was written in Java. It was nostalgic but also a bit unpleasant to use Java again after a year of using Kotlin for everything. Boilerplate everywhere. But I thought the development would take just a few days so I honored the tradition and kept the app purely in Java.
The app was built upon MVP architectural pattern. I personally used MVP pattern for years, but now with the release of Android Architecture Components and its ViewModel library, I find MVVM pattern superior. I spent a few days extracting copy-pasted code, updating library versions and removing unused fields, methods, classes, injections, and other unreachable code.
For such a small project, I personally expected smoother take-off.
Adding Kin SDK
Kin SDK allows developers to add an option for users to pay for digital goods, claim rewards for achieving goals, send money to other users, check their Kin balance, etc. The SDK doesn’t have a large public API. It consists of just a handful of static methods, but it is enough for most use cases. Most of them have throws keyword in their signature and are of blocking nature.
It made perfect sense to wrap their public API with RxJava so that every request could be easily composed in a reactive manner. No more try-catch blocks around each request and asynchronicity is now a piece of cake. Unsurprisingly, there are some ways to transfer money from or to an account. I will refer to them as Kin actions, blockchain actions, or even just actions.
Each of these actions requires a JWT token that is created and signed on our server. Client app then passes the signed token to the corresponding method and a few moments later (after getting processed by the blockchain) a confirmation of the chosen action is returned. Then we can send the confirmation back to the server to verify action was indeed processed successfully.
Game state, communication with server and malicious users
Originally the game logic was simple. Creating or responding to a match was just a matter of writing data into the Firebase. There was no money involved and nobody really cared when the game state stayed unfinished.
Introduction of payment/claiming into the app introduced several obstacles we didn’t foresee.
Now that users spend their money with each new match, users cannot get their money back if nobody replies to their challenge! We had to introduce an option to cancel a match and reclaim the original bet.
In addition to two actions
respondToMatch from the original app, we have a new one:
The dream scenario would be performing a single request to our server that would perform requested action. When the server would respond, the payment would be confirmed and the game state updated.
But that was just a dream. It is not possible to perform a blockchain action on behalf of the client.
Clients must perform the action on their device. There is no OAuth-like mechanism and there is no way around it. Thus, each action must be separated into two API calls with a blockchain request in between:
- Ask the server for JWT token.
- Send the token to Kin blockchain.
- Send the confirmation about successful payment/claim to the server.
You might see the problem with this approach immediately. What if we create a match, but never actually pay for it? That leads us to a question. When should we initialize the match?
We cannot just activate the match when we create the JWT token. We should wait for the confirmation about the payment before activating it. We decided to split
CREATED state into two:
CREATED. Until user didn’t pay the bet, no one else could modify the state of the match.
Very similar process happens when a user responds to a match. He also asks for JWT token, sends it to blockchain and subsequently sends a confirmation to the server. The states also had to be split into two:
FINISHED. Until the user didn’t pay the bet, no one else could modify the state of the match.
But wait! What if the responder is an asshole and never actually pays the bet? The match is then forever waiting for his confirmation and the challenger’s bet is stuck without an option to reclaim it.
We tried to make some special rules but always ended up hitting a wall.
We tried to add a timeout to each request. We tried to allow the challenger to cancel the match even when the state was
AWAITING_FINISH, but that could cause the responder’s bet to be lost in the void if he actually paid for the match.
It was a race condition and we didn’t know how to resolve the problem. We hated ourselves and the world for being unfair. We just knew that if there was something like OAuth, we could implement the dream scenario described above and such a situation could not happen.
We discussed the problem with another colleague and he got us on a right track — the key was to completely separate game state from payment states. As obvious it may seem now, it didn’t cross our minds back then.
The solution was to use 3 original states
CANCELLEDand add four new flags to each match:
responderClaimed and use the following rules:
- Matches are automatically created in
- Users may pay bets only when the state is
- Users may claim rewards only when state is
- Any of the players may call cancel when the match is in
CREATEDstate and the match state is changed to
- At a moment when both players have paid the bet, the state is automatically changed to
FINISHED, winner is decided and rewards adjusted.
This way both users may pay and claim independently of each other. That was not possible with the previous approach and was the main reason why it couldn’t reliably work. There is no longer a situation where the money of one of the players is stuck because of the other player.
I am bald now, but at least I feel victorious.
A demon called Firebase
The original app was built upon Firebase. Each time a user challenged someone, the app connected to the Firebase and stored information about a match there. When a user responded to a challenge, the app retrieved the information from the Firebase, resolved the winner locally and announced the winner.
It was a simple solution and for hackathon application it was sufficient. Moreover, this approach removed the need for a backend server. We quickly realized that users should not be able to write to the database. It posed a security risk. It would be bad if someone found a way to modify data in the Firebase directly.
For the new version, in which users can bet Kin with each match, a pure Firebase-based solution was insufficient. Crafty users (or hackers if you will) could access the Firebase and retrieve the symbol chosen by the challenger. This way the responder could always respond with an appropriate symbol and win 100% of the matches.
There are several ways out of this situation:
- Setting up more specific Firebase access rules. That seemed like the right way to tackle the problem as you can set up fairly complicated access rules within Firebase. But soon we found out that it is impossible to deny access to a node, but allow access to its parent node. If we grant access to the parent node, the user will also be able to access to all child nodes. Database rules won’t do the job.
- Denormalization of the Firebase data structures. This is kind of a remedy for the first option. In addition to specific rules, we would also have to denormalize the database by storing the symbol information in the separate structure. This solution would probably work at the cost of an increased complexity. We decided to go with option #3.
- Ask the server for the data and deny all access to the Firebase. This option meant to ditch the firebase access altogether and ask for the data from the server. The server just returns the data from the firebase and removes the symbol data if the game is not finished yet.
Getting rid of the Firebase was unexcepted, considering it was the central point of the original app. First, we removed write access and eventually read access too. In the end, the only usage of Firebase was the retrieval of the user’s name, which is silly. Users’ names can be easily retrieved from Facebook SDK and so we removed Firebase dependency altogether.
There are several lessons that I have learned with this project.
Although Firebase is usually a quick way to achieve some requirements, in our case, it really slowed us down. Over the course of development, we have been limiting the access to Firebase and in the final version of the new app, dependency to Firebase has been completely removed. All data is retrieved through the server. Server internally accesses the firebase from legacy reasons, which is quirky. It would have been much better to just store the data in the relational database.
The code I got to work with was outdated and messy. It was difficult to estimate the scope of changes at the start, but now I know it would have been better if I just started the application from scratch with new tools and better architecture in mind.
Kotlin greatly reduces the boilerplate and in my opinion, it is much more idiomatic. Concerning MVP architecture, I personally don’t like the bidirectional flow of data between View and Presenter and prefer to work with MVVM. Furthermore, Repository pattern would be useful for some parts of the app as well.
I run a few git commands to get statistics about the project:
- The latest version has a total of 8284 lines of code.
- Over the course of development, 160 files changed with 5947 insertions and 3898 deletions.
That means only about 28% of the original app’s code remained untouched. If I knew the number at the start, I would not hesitate and started from scratch.
When I was assigned the task to implement blockchain into the app, I thought I would get my hands dirty working with low-level API. But thanks to the Kin SDK (or ‘because of’?) I didn’t have a chance. The SDK provides just high-level functions that handle everything seamlessly. I’d like to get more insight about how does it work under the hood…
There was only one inconvenience using the SDK — absence of OAuth-like mechanism. It resulted in fairly complicated client-server communication that could otherwise be much simpler. But due to the nature of blockchain, it might not be even possible. The kin blockchain is built upon Ethereum and as far as I know, Ethereum distributes work between connected devices. When we perform a blockchain action, does the app internally perform some computations? I tried to measure CPU usage of the app when it performed blockchain action, but it was unmeasurable (or I just don’t know how to use the profiler tool). Nonetheless, it was an interesting experience to try out something new.
If anyone is interested in the application, feel free to try it out! The new version is available here.