Applications over Freenet: a Decentralized, Anonymous Gaming API?
In this article, I'll tell you how to write a simple, generic gaming API on top of Freenet using the shiniest new Freenet APIs and design patterns. Why write games over Freenet? Well, you can play without a central server, without having to worry about attackers falsifying moves and without knowing who you're playing against! Some people question the usefulness of these features. The real reason is it's more fun to implement than a relational database, and I'm not exactly getting paid for this. But wait, is Freenet real time? Is it fast enough to play a satisfying game of anonymous Quake on top of it? No, it's about as fast as e-mail or a heavily loaded web server, depending on the weather. Remember the part about us being crazy and not getting paid.
There are lots of ways to interface with your node; I won't cover them all here. Anyone who can use the Java Servlet API and one of the client APIs can write a new interface that runs inside the node. The Freenet HTTP Proxy (FProxy) is written this way, for instance. The interface I prefer for talking to my node is XML-RPC. There are XML-RPC libraries in at least 21 languages and some other things that may or may not be languages; I couldn't tell.
There are four different APIs exposed via XML-RPC in the Freenet reference implementation. The Util API supplies utility methods for determining the version of your node and other sundry items that don't concern us. The Simple API provides a one-line method call to insert and request files but is not designed to handle big files. The Chunked API allows for chunked retrieval of large files. The Streaming API allows for efficient streaming retrieval of data. I'm going to cover only the Simple API, as it is completely sufficient for our purposes.
Even though XML-RPC client can be written in 21 languages, I like Java best, so all of the examples will be in Java. If you're writing your code in Java, the power of interfaces means you don't have to think about XML-RPC at all. If you want to call the API directly (for instance, if you're writing code for a plugin that will run inside the node), then you instantiate a LocalSimpleClient. If you want to call the API via XML-RPC (for a standalone client), you then instantiate a RemoteSimpleClient. Either way, the rest of your code looks the same.
Using a LocalSimpleClient involves some parameter setting that isn't very interesting, and useful only if you're developing a node plugin, so I'll just cover RemoteSimpleClient. You instantiate a RemoteSimpleClient with a URL pointing it to the XML-RPC server. If your XML-RPC server is running on the default port as of the 0.3.8.1 Freenet release, your code should look like this:
SimpleClient client = new RemoteSimpleClient("http://localhost:6690");
Freenet is essentially a distributed hard drive with various optimizations and security features. As such, the Simple API looks like a Hashtable with a mysterious extra parameter, HTL. This parameter is the "Hops to Live" or, in other words, how many nodes you want to search before you give up. This number can be tricky to guess. The larger it is, the longer you have to wait for it to timeout if a file isn't in the network. However, the smaller it is, the greater the chance is that you won't find a file when it is indeed in the network. I recommend you set it high (say 100) and learn Zen-like patience.
Apart from guessing an HTL parameter, requesting and inserting files using the Simple API is simple and obvious by looking at the interface, but it's not anything you couldn't do from the command line. For a more exciting project, we must implement our gaming API.
A gaming API needs to: 1) track who wants to play, who is playing and who is watching, etc.; 2) match prospective players to start games; 3) route moves to players of a game; 4) provide an engine to check for validity of moves, states and score players; 5) provide a database of wins, losses and scores; and 6) a provide rating/ranking engine.
I'm supposed to be writing about Freenet more than gaming engines, so I'll only cover the first three needs at the moment. Number four should be done by a dedicated and modular piece of software so that different game engines can be plugged into the generic architecture. Items four through six contain issues of reputation and trusted relationships that should keep any self-respecting P2P programmer up at night pondering cheating, lying and control of information resources.
It's easy to register yourself as open for a game (see Listing 1). You simply construct a Freenet key (analogous to a filename or URL) that follows a well-known format and insert your desire for a match. Other game clients periodically request keys in this well-known format and make a catalog of open games. The format used by the EOF gaming engine is <<I>game>-request-<<I>seconds>-<<I>number>, where game is the name of the type of the game you're playing (to avoid a namespace collision with other games types), seconds is today's date in a funky format (to avoid a namespace collision with things inserted yesterday) and number is a unique number for that particular game (to avoid a namespace collision with other games).
Generating a key is easy, you need values for the date and the game number. To generate the game number, you first start at 0. You generate a key and try to insert it. If there is a collision, you then add 1 and try again. You keep doing this until the insert succeeds.
This can be a slow process if there are a lot of open games, and it's the reason for the date portion of the key. Since this portion changes every day, you can start over with 0 every day, thus keeping the number portion from growing arbitrarily large over time.
The seconds portion of the key is the most difficult to generate. You want a unique identifier for today's date. You could use a MM-DD-YY format. However, this is very inconvenient for programs that might want to look through several days worth of games, perhaps to generate a database of winners and losers. Enumerating through keys in this format requires the use of a calendar to know when to stop incrementing the day field, reset it and increment the month field.
Because of this, a "seconds since the epoch" format is used. A single function call exists in a majority of languages for obtaining this value. In Java, you do it with
long seconds = new Data().getTime()/1000
You have to divide by 1,000 because Java gives you milliseconds since the epoch. Unfortunately, this is not a unique identifier for today, but for a particular second of today. Luckily, there are 86,400 seconds in every day. Thus, to get a unique identifier for today, you simply compute seconds - (seconds % 86400).
You now have everything you need to generate a unique key for your match request. Now you just need something to put in it. In the EOF gaming API, we put your nickname. That way, when someone is browsing open games, they know who they will be playing against. Of course, this isn't a secure naming system. Players can change their names, copy other people's names and generally act obnoxious. Later in this article we'll rewrite the name system to be more secure, so don't get too attached to it.
Replying to an open game is simple: create a key in the format <<I>game>-reply-<<I>seconds>-<<I>number>, where all of the fields are the same as the request. It is important to note that you don't generate your own number here but, instead, use the same one as the request to which you want to reply. Put your nickname in the file and insert it under this key. If a collision occurs, then someone else has already entered the game and the game is closed. This method stated above obviously only works for two players. Extending it to N players is left as an exercise for the reader. Hopefully, your opponent's client is periodically checking for the insertion of a file with precisely the same key as the one you just inserted. Now it's time to insert moves.
The players take turns inserting moves under keys with the format <<I>game>-move-<<I>seconds>-<<I>number>, where the number starts at 0 and increments by one with each move. Whether players play in turn and whether their moves are legal is not part of this framework. These aspects depend greatly on what game you're playing and, as such, should be handled by software that knows about these things. There is, however, one fundamental and terrible flaw with this system of moves: your opponent can move for you and the game engine has no way of knowing who really made the move. It obviously would be desirable to have the moves be unspoofable. This is easy enough to do, but it requires more knowledge of Freenet features, in particular the formidable signed subspace key (SSK).
Freenet has a type of key that allows for self-verifying signed content. You make a public/private keypair, much as with PGP. You can then insert special keys that include your public key. Unless the accompanying data is properly signed with your private key, it can't be requested. This is useful when you want to ensure that a group of documents are all published by the same author, exactly the case with the gaming API.
So you first need to generate a keypair. You can do this through the Util API (available in Java or via XML-RPC). Invoking
returns a string containing your public and private keys (respectively), separated by a comma.
Now, you need to insert your next move using an SSK. The key to insert is SSK@<<I>pubkey>,<<I>privkey>/<<I>name>, where name is the key previously used to record moves. Since the move is stored under an SSK, no one can insert this key unless they have your private key. Therefore, your opponent can't spoof your moves.
Unfortunately, your opponents can no longer find out what your moves are because they don't know what keys they're under. Quite a pickle indeed! The format for an SSK that you want to request is SSK@<<I>pubkey>/<<I>name>. You only need the private key for insertion. It is, after all, supposed to be a secret. Your opponent can generate the name portion of the key, as we were doing in the previous section on inserting moves. So all you need to communicate is your public key.
Remember how I said we were going to replace the insecure naming system? Well now is the time. Instead of putting your nickname in the request and response files, use your public key. Now your opponent knows who you are in an unspoofable way. Sure, someone else can put your public key in a request, but they can't insert moves using that public key (unless they also have your private key), so they can't actually play as you. That means identities in the gaming system are unspoofable. Every time you play with someone with the same public key, you know they are the same person as before. Unfortunately, referring to your chess partner as "my good buddy p0EFqjmDioSqKmYYORPrClUepi4QAgE" is somehow unsatisfying. A good gaming client should therefore let you attach more meaningful names to your opponents.
Even though you can no longer falsify moves, you can still make illegal moves. If your gaming engine won't let you do that, you can modify it. If my gaming engine won't let you, then so what? You've still made the move. How do we resolve the game? Playing a game is a voluntary activity, so the proper response to invalid moves is to stop playing with that person. This doesn't work out so well when rankings are involved. If I begin every game by declaring I win, as long as there are gullible new players I can be number one. There are other nasty tricks, such as simply refusing to finish a game when you know you're going to lose. All of this should be taken into account if an engine is created to score players. Such a device would look through all of the requests and responses to determine what games were played on a particular day. It would then look through all of the moves for a particular game, check for validity of moves and determine who won, recording invalid moves and unfinished games as black marks on a player. Luckily, once files are put into Freenet, they can't be modified or deleted, even by the person that published them, so after-the-fact modifications of games are not possible (at least they are very hard to do). The scoring engine would then use the gathered data to score, rate, rank and admonish players. It could store its output somewhere in Freenet for players to view. Of course, scoring engines can be biased if their authors are also players. For this reason, choosing which scoring engine to believe is a matter of trust.
If you happen to like using a system that is not real time and does not guarantee the accessibility or permanence of its data to write distributed, anonymous, secure applications, then you'll probably like Freenet. It has built-in features for unspoofable communication. You can write applications for it in 21 different languages. You can play games totally anonymously, without a central server, and it's faster than play-by-mail games.
If, on the other hand, you think that we're a bunch of crazy people with too much time on our hands, then you should wait until IRC over Freenet comes out.
You probably think I'm joking.
Brandon Wiley cofounded the Freenet Project three years ago. Recently, he cofounded the Everything Over Freenet (EOF) project to implement interesting internet protocols over Freenet. He is currently writing plays and developing distributed application infrastructures in Austin, Texas.