Skip to content

Commit

Permalink
Test deploy, init, hostGame, and joinGame method & Fix missing securi…
Browse files Browse the repository at this point in the history
…ty checks
  • Loading branch information
Shigoto-dev19 committed Feb 15, 2024
1 parent 53ec3d0 commit 502c669
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 19 deletions.
188 changes: 184 additions & 4 deletions src/Battleships.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,187 @@
// import { battleships } from './battleships';
import { Battleships } from './Battleships';
import {
Field,
Mina,
AccountUpdate,
PrivateKey,
PublicKey,
UInt8,
} from 'o1js';
import { BoardUtils } from './client';

describe('battleships.js', () => {
describe('battleships()', () => {
it.todo('should be correct');
const proofsEnabled = false;

async function localDeploy(zkapp: Battleships, deployerKey: PrivateKey, zkappPrivateKey: PrivateKey) {
const deployerAccount = deployerKey.toPublicKey();
const tx = await Mina.transaction(deployerAccount, () => {
AccountUpdate.fundNewAccount(deployerAccount);
zkapp.deploy();
});

await tx.prove();
await tx.sign([deployerKey, zkappPrivateKey]).send();
}

async function initializeGame(zkapp: Battleships, deployerKey: PrivateKey) {
const deployerAccount = deployerKey.toPublicKey();

// deployer initializes zkapp
const initTx = await Mina.transaction(deployerAccount, () => {
zkapp.initGame();
});

await initTx.prove();
// await initTxn.sign([deployerKey]).send();
}


describe('Battleships Game Tests', () => {

let player1Key: PrivateKey,
player2Key: PrivateKey,
zkappAddress: PublicKey,
zkappPrivateKey: PrivateKey,
zkapp: Battleships;

beforeAll(async () => {
if (proofsEnabled) await Battleships.compile();

// setup local blockchain
const Local = Mina.LocalBlockchain({ proofsEnabled });
Mina.setActiveInstance(Local);

// Local.testAccounts is an array of 10 test accounts that have been pre-filled with Mina
player1Key = Local.testAccounts[0].privateKey;
player2Key = Local.testAccounts[1].privateKey;

// zkapp account
zkappPrivateKey = PrivateKey.random();
zkappAddress = zkappPrivateKey.toPublicKey();
zkapp = new Battleships(zkappAddress);
});

describe('Deploy and initialize Battleships zkApp', () => {
it('Generate and Deploy `Battleships` smart contract', async () => {
await localDeploy(zkapp, player1Key, zkappPrivateKey);
});

it('Initialize game', async () => {
await initializeGame(zkapp, player1Key);

const player1Id = zkapp.player1Id.get();
expect(player1Id).toEqual(Field(0));

const player2Id = zkapp.player2Id.get();
expect(player2Id).toEqual(Field(0));

const turns = zkapp.turns.get();
expect(turns).toEqual(UInt8.from(0));


const hitHistory = zkapp.hitHistory.get();
expect(hitHistory).toEqual(Field(0));
});
});

describe('hostGame method tests', () => {
it('should host a game and update player1Id', async () => {
const hostBoard = [
[0, 0, 0],
[0, 1, 0],
[0, 2, 0],
[0, 3, 0],
[0, 4, 0],
];

const hostSerializedBoard = BoardUtils.serialize(hostBoard);
let hostGameTx = await Mina.transaction(player1Key.toPublicKey(), () => {
zkapp.hostGame(hostSerializedBoard);
});

await hostGameTx.prove();
await hostGameTx.sign([player1Key]).send();

// compute player1Id locally
const computedHostId = BoardUtils.generatePlayerId(hostSerializedBoard, player1Key.toPublicKey());

// fetch the updated player1Id on-chain
const hostId = zkapp.player1Id.get()

// assert that the computed host ID is compliant with the ID stored on-chain
expect(computedHostId).toEqual(hostId);
});

it('should prevent other players to re-host a game', async () => {
const meddlerBoard =[
[9, 0, 1],
[9, 5, 1],
[6, 9, 0],
[6, 8, 0],
[7, 7, 0],
];

const meddlerSerializedBoard = BoardUtils.serialize(meddlerBoard);
const meddlerHostTX = async () => {
let hostGameTx = await Mina.transaction(player2Key.toPublicKey(), () => {
zkapp.hostGame(meddlerSerializedBoard);
});

await hostGameTx.prove();
await hostGameTx.sign([player2Key]).send();
}

expect(meddlerHostTX).rejects.toThrowError('This game has already a host!');
});
});

describe('joinGame method tests', () => {
it('should join a game and update player2Id', async () => {
const joinerBoard =[
[9, 0, 1],
[9, 5, 1],
[6, 9, 0],
[6, 8, 0],
[7, 7, 0],
];

const joinerSerializedBoard = BoardUtils.serialize(joinerBoard);
let joinGameTx = await Mina.transaction(player2Key.toPublicKey(), () => {
zkapp.joinGame(joinerSerializedBoard);
});

await joinGameTx.prove();
await joinGameTx.sign([player2Key]).send();

// compute player2Id locally
const computedjoinerId = BoardUtils.generatePlayerId(joinerSerializedBoard, player2Key.toPublicKey());

// fetch the updated player1Id on-chain
const joinerId = zkapp.player2Id.get()

// assert that the computed joiner ID is compliant with the ID stored on-chain
expect(computedjoinerId).toEqual(joinerId);
});

it('should prevent other players to join a full game', async () => {
const meddlerBoard =[
[9, 0, 1],
[9, 5, 1],
[6, 9, 0],
[6, 8, 0],
[7, 7, 0],
];

const meddlerSerializedBoard = BoardUtils.serialize(meddlerBoard);
const meddlerJoinTX = async () => {
let joinGameTx = await Mina.transaction(player1Key.toPublicKey(), () => {
zkapp.joinGame(meddlerSerializedBoard);
});

await joinGameTx.prove();
await joinGameTx.sign([player1Key]).send();
}

expect(meddlerJoinTX).rejects.toThrowError('This game is already full!');
});
});
});
39 changes: 26 additions & 13 deletions src/Battleships.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
Field,
Bool,
UInt8,
state,
State,
Expand All @@ -20,8 +19,6 @@ import {
export { Battleships }

class Battleships extends SmartContract {
@state(Bool) joinable = State<Bool>();
@state(Bool) finished = State<Bool>();
@state(Field) player1Id = State<Field>();
@state(Field) player2Id = State<Field>();
@state(UInt8) turns = State<UInt8>();
Expand All @@ -30,32 +27,51 @@ class Battleships extends SmartContract {

initGame() {
super.init();
this.joinable.set(Bool(true));
this.finished.set(Bool(false));
this.player1Id.set(Field(0));
this.player2Id.set(Field(0));
this.turns.set(UInt8.from(0));
this.hitHistory.set(Field(0));
}

@method hostGame(serializedBoard1: Field) {
// fetch on-chain player1 ID
const host = this.player1Id.getAndRequireEquals();

/**
* Assert that hostID is not updated yet.
* !Make sure nobody tampers with the host ID once updated!
*/
host.assertEquals(0, "This game has already a host!");

// assert that host ships placement is valid
const boardHash1 = BoardCircuit.validateBoard(serializedBoard1);

// calculate host ID & store it on-chain
const hostId = Poseidon.hash([boardHash1, ...this.sender.toFields()]);
this.player1Id.set(hostId);
}

@method joinGame(serializedBoard2: Field) {
this.joinable.getAndRequireEquals().assertTrue('This game is already full!');
//TODO? check if the game is hosted
//TODO? refer to each game to be joinable by a gameID

// fetch on-chain player2 ID
const joiner = this.player2Id.getAndRequireEquals();

// assert that no one has already joined the game
joiner.assertEquals(0, 'This game is already full!');

// assert that joiner ships placement is valid
const boardHash2 = BoardCircuit.validateBoard(serializedBoard2);

// calculate joiner ID & store it on-chain
const joinerId = Poseidon.hash([boardHash2, ...this.sender.toFields()]);

this.player2Id.set(joinerId);

// set joinable to false to prevent calling this method
this.joinable.set(Bool(false));
}

@method firstTurn(target: Field) {
//TODO assert only player1 to call this method

const turns = this.turns.getAndRequireEquals();
turns.assertEquals(0, "Opening attack can only be played at the beginning of the game!");

Expand Down Expand Up @@ -127,9 +143,6 @@ class Battleships extends SmartContract {
//TODO Add player client class
//TODO Reset game when finished

//TODO Track joinability following turn counter
//TODO hash player address + board hash

//TODO? Emit event following game actions

//? 1. Save adversary encrypted boards on-chain
Expand Down
10 changes: 8 additions & 2 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Bool,
Poseidon,
Provable,
PublicKey,
} from 'o1js';

export {
Expand Down Expand Up @@ -55,6 +56,11 @@ class BoardUtils {
const deserializedBoard = BoardUtils.deserialize(serializedBoard);
return BoardUtils.hash(deserializedBoard);
}

static generatePlayerId(serializedBoard: Field, playerAddress: PublicKey) {
const boardHash = BoardUtils.hashSerialized(serializedBoard);
return Poseidon.hash([boardHash, ...playerAddress.toFields()]);
}
}
class BoardCircuit {
static validateShipInRange(ship: Field[], shipLength: number, errorMessage: string) {
Expand Down Expand Up @@ -84,7 +90,6 @@ class BoardCircuit {
let index = ship[0].add(ship[1].mul(10));
for(let i=0; i<shipLength; i++) {
let coordinate = index.add(i).add(increment);
//? fixed => toBoolean() => not provable
let check = Provable.witness(Bool, () => {
let collisionExists = boardMap.some(item => item.equals(coordinate).toBoolean());
return Bool(collisionExists)
Expand Down Expand Up @@ -203,4 +208,5 @@ class AttackCircuit {
// Provable.runAndCheck(() => {
// let hit = AttackCircuit.attack(BoardUtils.parse(player1Board), shot.map(Field));
// });
// console.timeEnd('attack witness');
// console.timeEnd('attack witness');

0 comments on commit 502c669

Please sign in to comment.