A minimally working demonstration of a Private Set Intersection (PSI).
The aim is to prove this can work in a Real Time Strategy game, as shown in the research paper OpenConflict: Preventing Real Time Map Hacks in Online Games.
There is a live demo here: PSI Demo @ Vercel.
See the project roadmap for what's shipped and what's planned next.
C++ implementation: there is a C++ port of this project at EdwardAThomson/Private-Set-Intersection. It is the more actively developed implementation: the security fixes below landed there first, and it additionally has multithreaded scalar operations, a coarse-to-fine multi-level mesh cascade, an HTTP server for frontend integration, and benchmarking tools. This JS repo is the browser-native demo and is kept in protocol parity with it (same group, same hash-to-group, same membership-tag construction).
Recent updates: Ported two security fixes and a protocol improvement from the C++ port (Private-Set-Intersection). The hash-to-group function now produces points with unknown discrete log (ristretto255 element derivation from SHA-512, matching libsodium's crypto_core_ristretto255_from_hash); the old H(x)*G construction let a participant recover b*G from one run and enumerate the other set offline. Wire messages no longer carry any plaintext element; they contain only fixed 32-byte values (membership tags, blinded points, transformed points). Bob now sends one-way BLAKE3 membership tags instead of ciphertexts, so finalisation is O(A) hash lookups with no trial decryption. This is a breaking wire-format change.
Threat model: the protocol is private against honest-but-curious participants; a malicious participant can probe membership with fabricated inputs.
This code is a simple demonstration of how PSI calculations work.
One deficiency of the OpenConflict solution is that it has no protection against players who lie about their positions or visibility. I think the problem of lying is one that can be solved. Essentially, the players would reveal their all their position and visibility sets at the end of the game. Then players can check those against the rules of the game to ensure the calculations were correct and fit with the physics of the game.
Additionally, there would need to be a dispute resolution protocol on order to adjudicate in times when one player disagrees with another. This would happen when one player cheats and then denies it.
The overall top-level strategy is outlined in a blog I wrote in June 2020: Preventing cheaters in Fog Of War Games.
Here are a few of the technical Implementation choices that I made in this app.
- ristretto255 group (@noble/curves), with hash-to-group via SHA-512 and RFC 9496 element derivation (unknown discrete log)
- BLAKE3 one-way membership tags (@noble/hashes), derive-key mode with context
PSI-membership-tag-v1 - Multi-level grid system
- Web workers
These were picked for their speed of operation or to otherwise reduce overheads. Every wire entry is a fixed 32-byte value; no plaintext positions travel between the parties. In the future I probably need to look to Wasm, or otherwise creating a desktop app.
Run npm run selftest to check the protocol core (correct intersection, empty intersection for disjoint sets, and no input element appearing in any serialized message).
I put together a page that explains more of the details of what PSI is and how it works: PSI Explainer.
A landing page that describes the app and links to each of the demos below.
A demonstration of the protocol working. The units and visibility are static / simple points. Just hit the "Run PSI Protocol" button.
This is a simple visualization of simple point particles moving around inside a box (Bob's units). The visibility circles are static but sweep out a 2D area unlike the test on the home page. The code is inefficient at the moment but it shows that the protocol works with dynamic movement. The PSI code triggers every 5 seconds and is very slow to calculate (causes huge visualization lag).
In order to boost performance, the app converts positions and visibility to cells, which are a coarser representation of the pixels. 1 cell is 50x50 pixels. I later added multi-level meshing and web workers to boost performance. While the performance is much better, it is still a bit slow.
With the tag-based protocol, finalisation is a set lookup per Alice element rather than trial decryption of every packet, which removes the old quadratic step. The remaining cost is the elliptic curve scalar multiplications, which scale linearly with the number of cells.
There are is a traditional visibility calculation that is helpful to calibrate what should be seen. You should really open the console log to check the results.
I added a very simple demo game. It is a Roguelike where a player can move around a room to find monsters. Whenever a monster comes into the field of view, it will render only if the PSI calculation confirms it.
The code doesn't have great performance but I believe it works.
- Clone the repository:
git clone https://github.com/EdwardAThomson/psi-demo.git
cd psi-demo- Install dependencies:
npm install- Run the app:
npm startThis will start the app at http://localhost:3000.
This project is licensed under the Apache 2.0 License - see the LICENSE file for details.
Many thanks to the following people:
- Anuj Gupta, the researcher who shared this idea with me.
- AI coding assistants (Claude, ChatGPT) used along the way.
- Everyone at the Decentralized Gaming Association DGA Discord


