Skip to content

Commit

Permalink
Add board integrity test cases for firstTurn & attack methods + minor…
Browse files Browse the repository at this point in the history
… changes
  • Loading branch information
Shigoto-dev19 committed Feb 25, 2024
1 parent d094ac0 commit b969f21
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 38 deletions.
139 changes: 109 additions & 30 deletions src/Battleships.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ describe('Battleships Game Tests', () => {
targetTree: MerkleTree,
hitTree: MerkleTree,
hostBoard: number[][],
joinerBoard: number[][];
joinerBoard: number[][],
intruderBoard: number[][];

beforeAll(async () => {
if (proofsEnabled) await Battleships.compile();
Expand Down Expand Up @@ -90,7 +91,15 @@ describe('Battleships Game Tests', () => {
[6, 8, 0],
[7, 7, 0],
];


// set up a local intruder board for testing purposes
intruderBoard = [
[3, 2, 0],
[8, 4, 1],
[7, 0, 0],
[0, 9, 0],
[8, 8, 0],
];
});

describe('Deploy and initialize Battleships zkApp', () => {
Expand Down Expand Up @@ -123,7 +132,28 @@ describe('Battleships Game Tests', () => {
});

describe('hostGame method tests', () => {
it.todo('should reject host invalid board');
// We test only one invalid case because board correctness is tested separately
it('should reject host with invalid board', () => {
const invalidHostBoard = [
[9, 0, 1],
[9, 5, 1],
[6, 9, 0],
[6, 8, 0],
[10, 7, 0],
];

const invalidSerializedBoard = BoardUtils.serialize(invalidHostBoard);
const rejectedHostTX = async () => {
let hostGameTx = await Mina.transaction(hostKey.toPublicKey(), () => {
zkapp.hostGame(invalidSerializedBoard);
});

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

expect(rejectedHostTX).rejects.toThrowError('Ship5 is out of board range!');
});

it('should host a game and update player1Id', async () => {
const hostSerializedBoard = BoardUtils.serialize(hostBoard);
Expand All @@ -144,15 +174,7 @@ describe('Battleships Game Tests', () => {
expect(computedHostId).toEqual(hostId);
});

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

it('should prevent other players to re-host a game', async () => {
const intruderSerializedBoard = BoardUtils.serialize(intruderBoard);
const intruderHostTX = async () => {
let hostGameTx = await Mina.transaction(joinerKey.toPublicKey(), () => {
Expand All @@ -168,7 +190,27 @@ describe('Battleships Game Tests', () => {
});

describe('joinGame method tests', () => {
it.todo('should reject a joiner with invalid board');
it('should reject a joiner with invalid board', () => {
const invalidJoinerBoard = [
[9, 0, 2],
[9, 5, 1],
[6, 9, 0],
[6, 8, 0],
[7, 7, 0],
];

const invalidSerializedBoard = BoardUtils.serialize(invalidJoinerBoard);
const rejectedJoinTX = async () => {
let joinGameTx = await Mina.transaction(joinerKey.toPublicKey(), () => {
zkapp.joinGame(invalidSerializedBoard);
});

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

expect(rejectedJoinTX).rejects.toThrowError('Coordinate z should be 1 or 0!');
});

it('should join a game and update player2Id', async () => {
const joinerSerializedBoard = BoardUtils.serialize(joinerBoard);
Expand All @@ -190,17 +232,9 @@ describe('Battleships Game Tests', () => {
});

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

const intruderSerializedBoard = BoardUtils.serialize(intruderBoard);
const intruderJoinTX = async () => {
let joinGameTx = await Mina.transaction(hostKey.toPublicKey(), () => {
let joinGameTx = await Mina.transaction(intruderKey.toPublicKey(), () => {
zkapp.joinGame(intruderSerializedBoard);
});

Expand Down Expand Up @@ -233,7 +267,11 @@ describe('Battleships Game Tests', () => {
expect(rejectedFirstTurnTx).rejects.toThrowError(errorMessage);
}

it.todo('should reject a host with non-compliant board');
it('should reject a host with non-compliant board', async () => {
const errorMessage = 'Only the host is allowed to play the opening shot!';
// tamper with the host board --> use the intruder board instead --> break integrity
testInvalidFirstTurn(hostKey, intruderBoard, [1, 2], errorMessage);
});

it('should reject any caller other than the host', async () => {
const errorMessage = 'Only the host is allowed to play the opening shot!';
Expand Down Expand Up @@ -266,6 +304,30 @@ describe('Battleships Game Tests', () => {
targetTree.setLeaf(12n, Field(0));
});

it('should reject calling attack method before firstTurn', async () => {
let index = 0n;
let wTarget = targetTree.getWitness(index);
let targetWitness = new TargetMerkleWitness(wTarget);

let hTarget = hitTree.getWitness(index);
let hitWitness = new HitMerkleWitness(hTarget);

const serializedBoard = BoardUtils.serialize(hostBoard);
const serializedTarget = AttackUtils.serializeTarget([1, 2]);

const rejectedAttackTx = async () => {
let attackTx = await Mina.transaction(hostKey.toPublicKey(), () => {
zkapp.attack(serializedTarget, serializedBoard, targetWitness, hitWitness);
});

await attackTx.prove();
await attackTx.sign([hostKey]).send();
}

const errorMessage = 'Please wait for the host to play the opening shot first!';
expect(rejectedAttackTx).rejects.toThrowError(errorMessage);
});

it('should accept a valid TX and update target on-chain', async () => {
let index = zkapp.turns.get();
let w = targetTree.getWitness(index.toBigInt());
Expand All @@ -291,7 +353,7 @@ describe('Battleships Game Tests', () => {
targetTree.setLeaf(index.toBigInt(), storedTarget);
});

it('should reject calling the method more than once', async () => {
it('should reject calling firstTurn method more than once', async () => {
let index = zkapp.turns.get();
let w = targetTree.getWitness(index.toBigInt());
let targetWitness = new TargetMerkleWitness(w);
Expand All @@ -311,12 +373,10 @@ describe('Battleships Game Tests', () => {
const errorMessage = 'Opening attack can only be played at the beginning of the game!';
expect(rejectedFirstTurnTx).rejects.toThrowError(errorMessage);
});

it.todo('should reject calling attack method before firstTurn');
});

describe('attack method tests', () => {
async function testInvalidAttack(playerKey: PrivateKey, board: number[][], errorMessage: string, falseTargetIndex=false, falseHitIndex=false) {
async function testInvalidAttack(playerKey: PrivateKey, board: number[][], errorMessage: string, falseTargetIndex=false, falseHitIndex=false, target?: number[]) {
let index = zkapp.turns.get().toBigInt();
let wTarget = targetTree.getWitness(falseTargetIndex ? index + 1n : index);
let targetWitness = new TargetMerkleWitness(wTarget);
Expand All @@ -325,7 +385,7 @@ describe('Battleships Game Tests', () => {
let hitWitness = new HitMerkleWitness(hTarget);

const serializedBoard = BoardUtils.serialize(board);
const serializedTarget = AttackUtils.serializeTarget([1, 2]);
const serializedTarget = AttackUtils.serializeTarget(target ?? [1, 2]);

const rejectedAttackTx = async () => {
let attackTx = await Mina.transaction(playerKey.toPublicKey(), () => {
Expand Down Expand Up @@ -384,8 +444,15 @@ describe('Battleships Game Tests', () => {
expect(zkapp.turns.get().toBigInt()).toEqual(index + 1n);
}

it.todo('should reject eligible player with non-compliant board: host');
it.todo('should reject eligible player with non-compliant board: joiner');
it('should reject an invalid target: x coordinate', async () => {
const errorMessage = 'Target x coordinate is out of bound!';
testInvalidAttack(joinerKey, joinerBoard, errorMessage, false, false, [12, 5]);
});

it('should reject an invalid target: y coordinate', async () => {
const errorMessage = 'Target y coordinate is out of bound!';
testInvalidAttack(joinerKey, joinerBoard, errorMessage, false, false, [4, 13]);
});

it('should reject an eligible player from taking a turn out of sequence', async () => {
const errorMessage = 'You are not allowed to attack! Please wait for your adversary to take action!';
Expand Down Expand Up @@ -429,11 +496,23 @@ describe('Battleships Game Tests', () => {
hitTree.setLeaf(3n, Field(0));
});

it('should reject eligible player with non-compliant board: joiner', async () => {
const errorMessage = 'You are not allowed to attack! Please wait for your adversary to take action!';
// use intruder board instead to break integrity compliance
await testInvalidAttack(joinerKey, intruderBoard, errorMessage);
});

// player2 turn --> turn = 1
it('should accept a valid attack TX and update state on-chain: 1st check', async () => {
await testValidAttack(joinerKey, joinerBoard, [0, 0], false, [0, 0]);
});

it('should reject eligible player with non-compliant board: host', async () => {
const errorMessage = 'You are not allowed to attack! Please wait for your adversary to take action!';
// use intruder board instead to break integrity compliance
await testInvalidAttack(hostKey, intruderBoard, errorMessage);
});

// player1 turn --> turn = 2
it('should accept a valid attack TX and update state on-chain: 2nd check', async () => {
await testValidAttack(hostKey, hostBoard, [6, 8], true, [0, 1]);
Expand Down
17 changes: 9 additions & 8 deletions src/Battleships.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,14 @@ class Battleships extends SmartContract {
player2Id,
);

// deserialize board, also referred as ships
let deserializedBoard = BoardUtils.deserialize(serializedBoard);

// assert that the current player should be the sender
let senderBoardHash = BoardUtils.hash(deserializedBoard);
let senderId = Poseidon.hash([senderBoardHash, ...this.sender.toFields()]);
senderId.assertEquals(currentPlayerId, "You are not allowed to attack! Please wait for your adversary to take action!");

// assert root index compliance with the turn counter
const targetWitnessIndex = targetWitness.calculateIndex();
targetWitnessIndex.assertEquals(turns.value, "Target storage index is not compliant with turn counter!");
Expand All @@ -171,14 +179,6 @@ class Battleships extends SmartContract {
let storedHitRoot = this.hitRoot.getAndRequireEquals();
storedHitRoot.assertEquals(currentHitRoot, 'Off-chain hit merkle tree is out of sync!');

// deserialize board, also referred as ships
let deserializedBoard = BoardUtils.deserialize(serializedBoard);

// assert that the current player should be the sender
let senderBoardHash = BoardUtils.hash(deserializedBoard);
let senderId = Poseidon.hash([senderBoardHash, ...this.sender.toFields()]);
senderId.assertEquals(currentPlayerId, "You are not allowed to attack! Please wait for your adversary to take action!");

/**
* 1. Fetch adversary's serialized target from previous turn.
* 2. Deserialize target.
Expand Down Expand Up @@ -241,6 +241,7 @@ class Battleships extends SmartContract {
//TODO - Simulate game in tests
//TODO - Add player client class
//TODO Reset game when finished(keep state transition in mind)
//TODO Add nullifer for target to prevent player for attacking the same target more than once

//TODO? Emit event following game actions

Expand Down
3 changes: 3 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ class BoardUtils {
return Poseidon.hash([boardHash, ...playerAddress.toFields()]);
}
}

//TODO: Add "invalid board" message inside assertion logs
class BoardCircuit {
static validateShipInRange(ship: Field[], shipLength: number, errorMessage: string) {
// horizontal check: z=ship[2]=0
Expand All @@ -79,6 +81,7 @@ class BoardCircuit {
}

// verify z is binary
//TODO infer which adding another argument for zErrorMessage
ship[2].assertLessThanOrEqual(1, 'Coordinate z should be 1 or 0!');

const isInRange = Provable.if(ship[2].equals(1), checkVertical(), checkHorizontal());
Expand Down

0 comments on commit b969f21

Please sign in to comment.