diff --git a/static/js/modules/interface/dom.js b/static/js/modules/interface/dom.js
index 76daedf..457c95e 100644
--- a/static/js/modules/interface/dom.js
+++ b/static/js/modules/interface/dom.js
@@ -3,12 +3,12 @@ import { Region } from "./map.js";
import { Packet } from "./packet.js";
export function unlockMapDom() {
- Object.values(REGIONS).forEach((region) => {
- if (!allRegionsClaimed() && region.owner === null) {
+ Object.values(Region.getAllRegions()).forEach((region) => {
+ if (!Region.allRegionsClaimed() && region.owner === null) {
document
.querySelector(`.node[data-name=${region.name}] .actions`)
.classList.remove("hidden");
- } else if (region.owner === us) {
+ } else if (region.owner === game.us) {
document
.querySelector(`.node[data-name=${region.name}] .actions`)
.classList.remove("hidden");
@@ -36,7 +36,7 @@ function updateMapDom() {
e.classList.remove("hidden");
});
- if (us.isPlaying) {
+ if (game.us.isPlaying) {
document.querySelector("#end-turn").classList.remove("hidden");
} else {
document.querySelector("#end-turn").classList.add("hidden");
@@ -49,6 +49,8 @@ function updateMapDom() {
element.style.backgroundColor =
region.owner === null ? "white" : region.owner.getColor();
}
+
+ showRemainingReinforcements();
}
document.addEventListener("gameStateUpdate", () => {
@@ -57,6 +59,8 @@ document.addEventListener("gameStateUpdate", () => {
}
});
document.addEventListener("gameStateUpdate", updateMapDom);
+document.addEventListener("playerChange", updateMapDom);
+document.addEventListener("turnProgress", updateMapDom);
function updatePlayerDom() {
let list = document.querySelector("#playerList");
@@ -99,7 +103,7 @@ function updatePlayerDom() {
document.addEventListener("addPlayer", updatePlayerDom);
document.addEventListener("removePlayer", updatePlayerDom);
document.addEventListener("updatePlayer", updatePlayerDom);
-document.addEventListener("gameStateUpdate", updatePlayerDom);
+document.addEventListener("playerChange", updatePlayerDom);
document.addEventListener("DOMContentLoaded", () => {
document.querySelector("#ready-button").addEventListener("click", async (ev) => {
@@ -207,17 +211,18 @@ function showRemainingReinforcements() {
if (game.isPregame()) {
document.querySelector(
"#remaining-reinforcements"
- ).innerHTML = `Remaining placements: ${reinforcementsRemaining()}`;
+ ).innerHTML = `Remaining placements: ${Region.reinforcementsRemaining()}`;
} else if (game.isPlaying()) {
document.querySelector(
"#remaining-reinforcements"
).innerHTML = `Remaining placements: ${
- currentPlayer().reinforcementsAvailable - currentPlayer().reinforcementsPlaced
+ game.currentPlayer().reinforcementsAvailable -
+ game.currentPlayer().reinforcementsPlaced
}`;
}
}
-function showDefenseDom(region) {
+export function showDefenseDom(region) {
const modal = document.querySelector("#defense-modal");
modal.querySelector("span").textContent = region;
modal.classList.remove("hidden");
diff --git a/static/js/modules/interface/game.js b/static/js/modules/interface/game.js
index 3357022..5791874 100644
--- a/static/js/modules/interface/game.js
+++ b/static/js/modules/interface/game.js
@@ -32,6 +32,10 @@ export class Game {
document.dispatchEvent(event);
}
+ playerCount() {
+ return Object.values(this.players).length;
+ }
+
currentPlayer() {
return Object.values(this.players).filter((p) => p.isPlaying)[0];
}
diff --git a/static/js/modules/interface/main.js b/static/js/modules/interface/main.js
index 7a79a07..352c9e5 100644
--- a/static/js/modules/interface/main.js
+++ b/static/js/modules/interface/main.js
@@ -3,12 +3,13 @@ import { Random } from "./random.js";
import { Barrier } from "./barrier.js";
import { Packet } from "./packet.js";
import { Game } from "./game.js";
+import { Region } from "./map.js";
import "./dom.js";
export const ID = window.crypto.randomUUID();
export const game = new Game();
export let socket;
-let random;
+export let random;
let barrier;
window.paillier = generate_keypair();
window.rsa = generate_rsa_keypair();
@@ -42,8 +43,8 @@ document.addEventListener("DOMContentLoaded", () => {
} else {
let sig = BigInt(packet.sig);
// decrypt and compare signature
- let dehash = sender.rsaPubKey.encrypt(sig).toString(16);
- let hash = CryptoJS.SHA3(JSON.stringify(data)).toString();
+ let dehash = sender.rsaPubKey.encrypt(sig);
+ let hash = BigInt("0x" + CryptoJS.SHA3(JSON.stringify(data)).toString());
if (dehash !== hash) {
window.console.error(`Signature invalid! Ignoring packet ${data.id}.`);
return;
@@ -138,6 +139,9 @@ document.addEventListener("ACT", async (ev) => {
} else {
if (await game.currentPlayer().act(data)) {
game.currentPlayer().endTurn();
+ } else {
+ const event = new CustomEvent("turnProgress");
+ document.dispatchEvent(event);
}
}
}
@@ -156,5 +160,8 @@ document.addEventListener("gameStateUpdate", async () => {
firstPlayer.isPlaying = true;
await barrier.wait();
+
+ const event = new CustomEvent("playerChange");
+ document.dispatchEvent(event);
}
});
diff --git a/static/js/modules/interface/map.js b/static/js/modules/interface/map.js
index 0c67e15..dfafd01 100644
--- a/static/js/modules/interface/map.js
+++ b/static/js/modules/interface/map.js
@@ -1,9 +1,11 @@
+import { game } from "./main.js";
+
let allPlaced = false;
// In standard Risk, this is 5
const _REINFORCEMENT_MULTIPLIER = 1;
-export const REGIONS = {};
+const REGIONS = {};
class Continent {
constructor(name) {
@@ -39,11 +41,10 @@ export class Region {
return 0;
} else {
let totalStrength = Object.values(REGIONS)
- .filter((region) => region.owner === us)
+ .filter((region) => region.owner === game.us)
.reduce((counter, region) => counter + region.strength, 0);
- let numPlayers = Object.values(players).length;
- return _REINFORCEMENT_MULTIPLIER * (10 - numPlayers) - totalStrength;
+ return _REINFORCEMENT_MULTIPLIER * (10 - game.playerCount()) - totalStrength;
}
}
@@ -55,11 +56,12 @@ export class Region {
(counter, region) => counter + region.strength,
0
);
- let numPlayers = Object.values(players).length;
allPlaced =
totalStrength >=
- numPlayers * _REINFORCEMENT_MULTIPLIER * (10 - numPlayers);
+ game.playerCount() *
+ _REINFORCEMENT_MULTIPLIER *
+ (10 - game.playerCount());
return allPlaced;
}
}
diff --git a/static/js/modules/interface/packet.js b/static/js/modules/interface/packet.js
index 8fa4734..0f355c0 100644
--- a/static/js/modules/interface/packet.js
+++ b/static/js/modules/interface/packet.js
@@ -52,15 +52,34 @@ export class Packet {
});
}
+ static createRandomCyphertext(sessionId, range, cipherText) {
+ return this._sign({
+ ...this._createBase("RANDOM"),
+ session: sessionId,
+ range: range,
+ stage: "CIPHERTEXT",
+ cipherText: cipherText,
+ });
+ }
+
+ static createRandomKey(sessionId, key) {
+ return this._sign({
+ ...this._createBase("RANDOM"),
+ session: sessionId,
+ stage: "DECRYPT",
+ cipherKey: key,
+ });
+ }
+
static createBarrierSignal() {
return this._sign(this._createBase("BARRIER"));
}
static createRegionClaim(region) {
- return {
+ return this._sign({
...this._createBase("ACT"),
region: region,
- };
+ });
}
static createAction(action, startRegion, endRegion, amount) {
diff --git a/static/js/modules/interface/player.js b/static/js/modules/interface/player.js
index 01d4dbc..988d489 100644
--- a/static/js/modules/interface/player.js
+++ b/static/js/modules/interface/player.js
@@ -1,6 +1,8 @@
import { Packet } from "./packet.js";
-import { socket } from "./main.js";
+import { socket, game, random } from "./main.js";
import { RsaPubKey } from "../crypto/rsa.js";
+import { Region } from "./map.js";
+import { showDefenseDom } from "./dom.js";
// Timeout to consider a player disconnected
const TIMEOUT = 30_000;
@@ -100,9 +102,6 @@ export class Player {
* @returns {boolean} Whether this player's turn has ended or not.
*/
async act(data) {
- console.log(`player: ${this.id}`);
- console.log(data);
-
if (this.turnPhase === PHASE_REINFORCE) {
if (data.region !== undefined) {
if (this.reinforce(data)) {
@@ -158,7 +157,7 @@ export class Player {
}
// If we're the defender, we need to send a packet to state our defense.
- if (defender.owner === us) {
+ if (defender.owner === game.us) {
showDefenseDom(defender.name);
}
@@ -223,8 +222,6 @@ export class Player {
// Reset the promises in case they attack again.
defender.owner.defenderPromise = null;
defender.owner.defenderAmount = null;
-
- updateDom();
}
async setDefense(amount) {
@@ -284,8 +281,9 @@ export class Player {
return Math.min(
3,
Math.floor(
- Object.values(REGIONS).filter((region) => region.owner === this).length /
- 3
+ Object.values(Region.getAllRegions()).filter(
+ (region) => region.owner === this
+ ).length / 3
)
);
}
@@ -307,10 +305,14 @@ export class Player {
endTurn() {
this.isPlaying = false;
this.nextPlayer().startTurn();
+
+ const event = new CustomEvent("playerChange");
+ document.dispatchEvent(event);
}
nextPlayer() {
- let sorted = Object.values(players).sort((a, b) => (a.id < b.id ? -1 : 1));
+ // todo move this to :class:Game
+ let sorted = Object.values(game.players).sort((a, b) => (a.id < b.id ? -1 : 1));
let ourIndex = sorted.findIndex((player) => player.id === this.id);
return sorted[(ourIndex + 1) % sorted.length];
diff --git a/static/js/modules/interface/random.js b/static/js/modules/interface/random.js
index ae10459..6b11331 100644
--- a/static/js/modules/interface/random.js
+++ b/static/js/modules/interface/random.js
@@ -1,4 +1,5 @@
-import { socket, ID, game } from "./main.js";
+import { socket, game } from "./main.js";
+import { Packet } from "./packet.js";
class RandomSession {
constructor(range) {
@@ -56,14 +57,10 @@ export class Random {
this.sessions[sessionId] = session;
- socket.emit("message", {
- type: "RANDOM",
- author: ID,
- session: sessionId,
- range: n,
- stage: "CIPHERTEXT",
- cipherText: session.cipherText(),
- });
+ socket.emit(
+ "message",
+ Packet.createRandomCyphertext(sessionId, n, session.cipherText())
+ );
return session;
}
@@ -90,13 +87,10 @@ export class Random {
Object.keys(game.players).length - 1
) {
// Step 3: release our key once all players have sent a ciphertext
- socket.emit("message", {
- type: "RANDOM",
- author: ID,
- session: data.session,
- stage: "DECRYPT",
- cipherKey: session.ourKey,
- });
+ socket.emit(
+ "message",
+ Packet.createRandomKey(data.session, session.ourKey)
+ );
}
} else if (stage === "DECRYPT") {
session.cipherKeys[data.author] = data.cipherKey;
diff --git a/whitepaper/Dissertation.pdf b/whitepaper/Dissertation.pdf
index f2c2f02..d466b40 100644
Binary files a/whitepaper/Dissertation.pdf and b/whitepaper/Dissertation.pdf differ
diff --git a/whitepaper/Dissertation.tex b/whitepaper/Dissertation.tex
index c23d1e9..9fe30ae 100644
--- a/whitepaper/Dissertation.tex
+++ b/whitepaper/Dissertation.tex
@@ -259,10 +259,14 @@ In particular, the final point allows for the use of purely JSON messages, which
\subsection{Message structure}
-Messages are given a fixed structure to make processing simpler. Each JSON message holds an \texttt{author} field, being the sender's ID, and an \texttt{action}, which at a high level dictates how each client should process the message.
+Messages are given a fixed structure to make processing simpler. Each JSON message holds an \texttt{author} field, being the sender's ID; a message ID to prevent replay attacks and associate related messages; and an \texttt{action}, which at a high level dictates how each client should process the message.
+
+The action more specifically is one of \texttt{ANNOUNCE}, \texttt{DISCONNECT}, \texttt{KEEPALIVE}, \texttt{RANDOM}, and \texttt{ACT}. The first three of these are used for managing the network by ensuring peers are aware of each other and know the state of the network. \texttt{RANDOM} is designated to be used by the shared-random-value subprotocol defined later. \texttt{ACT} is used by players to submit actions for their turn during gameplay.
Each message is also signed to verify the author. This is a standard application of RSA. A hash of the message is taken, then encrypted with the private key. This can be verified with the public key.
+RSA keys are accepted by peers on a first-seen basis.
+
\subsection{Paillier}
Paillier requires the calculation of two large primes for the generation of public and private key pairs. ECMAScript typically stores integers as floating point numbers, giving precision up to $2^{53}$. This is clearly inappropriate for the generation of sufficiently large primes.
@@ -362,7 +366,7 @@ Then, a proof for the following homologous problem can be trivially constructed:
\subsection{Application to domain}
-Players should prove a number of properties of their game state to each other to ensure fair play. These are as follows. \begin{itemize}
+Players should prove a number of properties of their game state to each other to ensure fair play. These are as follows. \begin{enumerate}
\item The number of reinforcements placed during the first stage of a turn.
\item The number of units on a region neighbouring another player.
@@ -372,10 +376,11 @@ Players should prove a number of properties of their game state to each other to
\item The number of units available for an attack/defence.
\item The number of units moved when fortifying.
-\end{itemize}
+\end{enumerate}
-Of these, the bottom two are, more specifically, range proofs. %todo is this grammar right?
-The top three can be addressed with the protocol we have provided however.
+(4) and (5) can be generalised further as range proofs.
+
+For (1), we propose the following communication sequence. The player submits pairs $(R, c_R)$ for each region they control, where $R$ is the region and $c_R$ is a cyphertext encoding the number of reinforcements to add to the region (which may be 0). Each player computes $c_{R_1} \cdot \ldots \cdot c_{R_n}$.
\bibliography{Dissertation}