Skip to content

Commit

Permalink
Merge pull request #33 from JetonDAO/decrypt-public-cards
Browse files Browse the repository at this point in the history
rest of decryption and betting
  • Loading branch information
arssly authored Sep 24, 2024
2 parents bd860bd + b4d8ad1 commit dcacee9
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 41 deletions.
164 changes: 125 additions & 39 deletions packages/ts-sdk/src/Game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
type PlacingBettingActions,
type Player,
PlayerStatus,
PublicCardRounds,
type TableInfo,
type offChainTransport,
} from "@src/types";
Expand All @@ -35,11 +36,17 @@ import {
type OnChainPlayerCheckedInData,
type OnChainPlayerPlacedBetData,
type OnChainPrivateCardsSharesData,
type OnChainPublicCardsSharesData,
type OnChainShuffledDeckData,
} from "./OnChainDataSource";
import { getUrlBytes, readData } from "./getURLBytes";
import { type GameEventMap, GameEventTypes } from "./types/GameEvents";
import {
type GameEventMap,
GameEventTypes,
type ReceivedPublicCardsEvent,
} from "./types/GameEvents";
import { calculatePercentage } from "./utils/calculatePercentage";
import { getGameStatus, getNextBettingRound, getNextPublicCardRound } from "./utils/convertTypes";

export type ZkDeckUrls = {
shuffleEncryptDeckWasm: string;
Expand Down Expand Up @@ -124,9 +131,23 @@ export class Game extends EventEmitter<GameEventMap> {
OnChainEventTypes.PRIVATE_CARDS_SHARES_RECEIVED,
this.receivedPrivateCardsShares,
);
this.onChainDataSource.on(
OnChainEventTypes.PUBLIC_CARDS_SHARES_RECEIVED,
this.receivedPublicCardsShares,
);
this.onChainDataSource.on(OnChainEventTypes.PLAYER_PLACED_BET, this.receivedPlayerBet);
}

private receivedPublicCardsShares = (data: OnChainPublicCardsSharesData) => {
if (!this.handState.privateCardShareProofs)
throw new Error("CardShareProofSource must already be present");
const cardIndexes = this.getCardIndexForRound(data.round);
this.handState.privateCardShareProofs.addProofs(data.sender, data.proofs);
if (this.handState.privateCardShareProofs.receivedAllProofsFor(cardIndexes)) {
this.decryptPublicCards(data.round, cardIndexes);
}
};

private receivedPrivateCardsShares = (data: OnChainPrivateCardsSharesData) => {
if (!this.handState.privateCardShareProofs)
throw new Error("CardShareProofSource must already be present");
Expand Down Expand Up @@ -168,7 +189,7 @@ export class Game extends EventEmitter<GameEventMap> {
}

public get numberOfPlayers() {
return this.gameState.players.length;
return this.gameState.players.filter((p) => p.status !== PlayerStatus.sittingOut).length;
}

public get myPlayer() {
Expand All @@ -190,6 +211,27 @@ export class Game extends EventEmitter<GameEventMap> {
return [this.myDistanceFromDealer * 2, this.myDistanceFromDealer * 2 + 1] as const;
}

public getCardIndexForRound(round: PublicCardRounds) {
const lastPrivateCardIndex = this.numberOfPlayers * 2;
let cardIndexes: number[];
switch (round) {
case PublicCardRounds.FLOP:
cardIndexes = [
lastPrivateCardIndex + 1,
lastPrivateCardIndex + 2,
lastPrivateCardIndex + 3,
];
break;
case PublicCardRounds.TURN:
cardIndexes = [lastPrivateCardIndex + 4];
break;
case PublicCardRounds.RIVER:
cardIndexes = [lastPrivateCardIndex + 5];
break;
}
return cardIndexes;
}

private gameStarted = (data: OnChainGameStartedData) => {
for (const playerId of data.players) {
const player = this.gameState.players.find((p) => p.id === playerId);
Expand Down Expand Up @@ -262,11 +304,17 @@ export class Game extends EventEmitter<GameEventMap> {
potAfterBet: Array.from(this.handState.pot),
});
const nextPlayer = this.handState.bettingManager.nextBettingPlayer;
if (!nextPlayer) {
const nextPublicCardRound = getNextPublicCardRound(data.bettingRound);
if (nextPlayer === null && nextPublicCardRound !== null) {
// TODO: betting round finished
this.createAndSharePublicKeyShares(nextPublicCardRound);
console.log("betting round finished");
return;
}
if (nextPlayer === null) {
//TODO: showdown
return;
}

// don't send awaiting bet to ui for small and big blind
if (data.action !== BettingActions.BIG_BLIND && data.action !== BettingActions.SMALL_BLIND) {
Expand All @@ -290,71 +338,109 @@ export class Game extends EventEmitter<GameEventMap> {
if (!this.handState.privateCardShareProofs)
throw new Error("CardShareProofSource must already be present");

this.handState.finalOutDeck = await this.onChainDataSource.queryLastOutDeck(this.tableInfo.id);
const numberOfCardsToCreateShareFor = this.numberOfPlayers * 2;
const cardIndexes = Array.from(new Array(this.numberOfPlayers * 2).keys());

const proofsAndShares = await this.createDecryptionShareProofsFor(cardIndexes);

this.handState.privateCardShareProofs.addProofs(this.playerId, proofsAndShares);

const myPrivateCardsIndexes = this.myPrivateCardIndexes;
const proofsToSend = proofsAndShares.filter(
(pas) => !myPrivateCardsIndexes.includes(pas.cardIndex),
);
this.onChainDataSource.privateCardsDecryptionShare(this.playerId, proofsToSend);
}

private async createDecryptionShareProofsFor(indexes: number[]) {
if (!this.elGamalSecretKey) throw new Error("elGamal secret key should be present");
if (!this.zkDeck) throw new Error("zkDeck should be present");
if (!this.handState.finalOutDeck) {
this.handState.finalOutDeck = await this.onChainDataSource.queryLastOutDeck(
this.tableInfo.id,
);
}
const proofPromises: Promise<{
proof: Groth16Proof;
decryptionCardShare: DecryptionCardShare;
}>[] = [];
for (let i = 0; i < numberOfCardsToCreateShareFor; i += 1) {
for (const index of indexes) {
proofPromises.push(
this.zkDeck.proveDecryptCardShare(this.elGamalSecretKey, i, this.handState.finalOutDeck),
this.zkDeck.proveDecryptCardShare(
this.elGamalSecretKey,
index,
this.handState.finalOutDeck,
),
);
}
const proofsAndShares = (await Promise.all(proofPromises)).map((s, i) =>
Object.assign({ cardIndex: i }, s),
);

this.handState.privateCardShareProofs.addProofs(this.playerId, proofsAndShares);

const myPrivateCardsIndexes = this.myPrivateCardIndexes;
const proofsToSend = proofsAndShares.filter(
(pas) => !myPrivateCardsIndexes.includes(pas.cardIndex),
Object.assign({ cardIndex: indexes[i] as number }, s),
);
this.onChainDataSource.privateCardsDecryptionShare(this.playerId, proofsToSend);
return proofsAndShares;
}

private decryptMyPrivateCards() {
if (!this.zkDeck) throw new Error("zkDeck must have been created by now!");
private async createAndSharePublicKeyShares(round: PublicCardRounds) {
if (!this.handState.privateCardShareProofs)
throw new Error("CardShareProofSource must already be present");
if (!this.handState.finalOutDeck) throw new Error("finalOutDeck must be present");

const [firstCardIndex, secondCardIndex] = this.myPrivateCardIndexes;
const proofsAndSharesOfFirstCard =
this.handState.privateCardShareProofs.getProofsFor(firstCardIndex);
const proofsAndSharesOfSecondCard =
this.handState.privateCardShareProofs.getProofsFor(secondCardIndex);
const cardIndexes = this.getCardIndexForRound(round);
const proofsAndShares = await this.createDecryptionShareProofsFor(cardIndexes);
this.handState.privateCardShareProofs.addProofs(this.playerId, proofsAndShares);
this.onChainDataSource.publicCardsDecryptionShare(this.playerId, proofsAndShares, round);
}

const firstPrivateCard = this.zkDeck?.decryptCard(
firstCardIndex,
private decryptCard(index: number) {
if (!this.handState.privateCardShareProofs) throw new Error("invalid call");
if (!this.handState.finalOutDeck) throw new Error("finalOutDeck must be present");
if (!this.zkDeck) throw new Error("zkDeck must have been created by now!");
const proofsAndSharesOfCard = this.handState.privateCardShareProofs.getProofsFor(index);
return this.zkDeck.decryptCard(
index,
this.handState.finalOutDeck,
proofsAndSharesOfFirstCard.map((pas) => pas.decryptionCardShare),
proofsAndSharesOfCard.map((pas) => pas.decryptionCardShare),
);
}

private decryptPublicCards(round: PublicCardRounds, indexes: number[]) {
const publicCards = indexes.map((index) => this.decryptCard(index));
this.emit(GameEventTypes.RECEIVED_PUBLIC_CARDS, {
round,
cards: publicCards,
} as ReceivedPublicCardsEvent);
this.initBettingRound(getNextBettingRound(round));
}

private decryptMyPrivateCards() {
const [firstCardIndex, secondCardIndex] = this.myPrivateCardIndexes;
const firstPrivateCard = this.decryptCard(firstCardIndex);
const secondPrivateCard = this.decryptCard(secondCardIndex);

const secondPrivateCard = this.zkDeck?.decryptCard(
secondCardIndex,
this.handState.finalOutDeck,
proofsAndSharesOfSecondCard.map((pas) => pas.decryptionCardShare),
);
this.emit(GameEventTypes.RECEIVED_PRIVATE_CARDS, {
cards: [firstPrivateCard, secondPrivateCard],
});
this.initBettingRound(BettingRounds.PRE_FLOP);
}

private initBettingRound(round: BettingRounds) {
this.handState.bettingManager = new BettingManager(
this.gameState.players,
this.gameState.players[this.gameState.dealer] as Player,
this.tableInfo,
this.myPlayer,
);
this.gameState.status = getGameStatus(round);
if (round === BettingRounds.PRE_FLOP) {
this.handState.bettingManager = new BettingManager(
this.gameState.players,
this.gameState.players[this.gameState.dealer] as Player,
this.tableInfo,
this.myPlayer,
);
}
if (!this.handState.bettingManager) throw new Error("illegal call of rounds");
this.handState.bettingManager.startRound(round);

const nextBettingPlayer = this.handState.bettingManager.nextBettingPlayer;
const nextPublicCardRound = getNextPublicCardRound(round);
if (nextBettingPlayer === null && nextPublicCardRound !== null) {
this.createAndSharePublicKeyShares(nextPublicCardRound);
return;
}
if (nextBettingPlayer === null) {
//TODO round finished
//TODO: showdown
return;
}
const isItMyTurn = nextBettingPlayer === this.myPlayer;
Expand Down
29 changes: 28 additions & 1 deletion packages/ts-sdk/src/OnChainDataSource.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EventEmitter } from "events";
import type { GameState } from ".";
import type { GameState, PublicCardRounds } from ".";
import { PieSocketTransport } from "./transport";
import {
type BettingActions,
Expand All @@ -17,6 +17,7 @@ export enum OnChainEventTypes {
SHUFFLED_DECK = "shuffled-deck",
PRIVATE_CARDS_SHARES_RECEIVED = "private-cards-shares",
PLAYER_PLACED_BET = "player-placed-bet",
PUBLIC_CARDS_SHARES_RECEIVED = "public-cards-shares",
}

export type OnChainPlayerCheckedInData = {
Expand Down Expand Up @@ -45,12 +46,19 @@ export type OnChainPlayerPlacedBetData = {
player: string;
};

export type OnChainPublicCardsSharesData = {
sender: string;
proofs: CardShareAndProof[];
round: PublicCardRounds;
};

type OnChainEventMap = {
[OnChainEventTypes.PLAYER_CHECKED_IN]: [OnChainPlayerCheckedInData];
[OnChainEventTypes.GAME_STARTED]: [OnChainGameStartedData];
[OnChainEventTypes.SHUFFLED_DECK]: [OnChainShuffledDeckData];
[OnChainEventTypes.PRIVATE_CARDS_SHARES_RECEIVED]: [OnChainPrivateCardsSharesData];
[OnChainEventTypes.PLAYER_PLACED_BET]: [OnChainPlayerPlacedBetData];
[OnChainEventTypes.PUBLIC_CARDS_SHARES_RECEIVED]: [OnChainPublicCardsSharesData];
};

export class OnChainDataSource extends EventEmitter<OnChainEventMap> {
Expand Down Expand Up @@ -114,6 +122,13 @@ export class OnChainDataSource extends EventEmitter<OnChainEventMap> {
this.emit(OnChainEventTypes.PLAYER_PLACED_BET, data);
},
);
this.pieSocketTransport.subscribe(
OnChainEventTypes.PUBLIC_CARDS_SHARES_RECEIVED,
(data: OnChainPublicCardsSharesData) => {
//TODO: this is different in transactions
this.emit(OnChainEventTypes.PUBLIC_CARDS_SHARES_RECEIVED, data);
},
);

if (this.gameState.players.length === 0) {
this.gameState.players.push({
Expand Down Expand Up @@ -154,6 +169,18 @@ export class OnChainDataSource extends EventEmitter<OnChainEventMap> {
});
}

async publicCardsDecryptionShare(
id: string,
proofs: CardShareAndProof[],
round: PublicCardRounds,
) {
this.pieSocketTransport.publish(OnChainEventTypes.PRIVATE_CARDS_SHARES_RECEIVED, {
sender: id,
proofs,
round,
});
}

async queryGameState() {
await new Promise((resolve) => setTimeout(resolve, 1500));
return this.gameState;
Expand Down
15 changes: 14 additions & 1 deletion packages/ts-sdk/src/types/GameEvents.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { PublicCardRounds } from ".";
import type { BettingActions, BettingRounds } from "./Betting";
import type { Player } from "./Player";

Expand All @@ -11,6 +12,7 @@ export enum GameEventTypes {
RECEIVED_PRIVATE_CARDS = "received-private-cards",
AWAITING_BET = "awaiting-bet",
PLAYER_PLACED_BET = "player-placed-bet",
RECEIVED_PUBLIC_CARDS = "received-public-cards",
}

export type DownloadProgressEvent = {
Expand All @@ -35,6 +37,15 @@ export type PrivateCardDecryptionStarted = Record<string, never>;
export type ReceivedPrivateCardsEvent = {
cards: [number, number];
};
export type ReceivedPublicCardsEvent =
| {
round: PublicCardRounds.FLOP;
cards: [number, number, number];
}
| {
round: PublicCardRounds.RIVER | PublicCardRounds.TURN;
cards: [number];
};

export type AwaitingBetEvent = {
bettingRound: BettingRounds;
Expand All @@ -59,7 +70,8 @@ export type GameEvents =
| PrivateCardDecryptionStarted
| ReceivedPrivateCardsEvent
| AwaitingBetEvent
| PlayerPlacedBetEvent;
| PlayerPlacedBetEvent
| ReceivedPublicCardsEvent;

export type GameEventMap = {
[GameEventTypes.DOWNLOAD_PROGRESS]: [DownloadProgressEvent];
Expand All @@ -71,4 +83,5 @@ export type GameEventMap = {
[GameEventTypes.RECEIVED_PRIVATE_CARDS]: [ReceivedPrivateCardsEvent];
[GameEventTypes.AWAITING_BET]: [AwaitingBetEvent];
[GameEventTypes.PLAYER_PLACED_BET]: [PlayerPlacedBetEvent];
[GameEventTypes.RECEIVED_PUBLIC_CARDS]: [ReceivedPublicCardsEvent];
};
5 changes: 5 additions & 0 deletions packages/ts-sdk/src/types/GameState.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { GameStatus } from "./GameStatus";
import type { Player } from "./Player";

export enum PublicCardRounds {
FLOP = "flop",
TURN = "turn",
RIVER = "river",
}
export interface GameState {
players: Player[];
dealer: number;
Expand Down
Loading

0 comments on commit dcacee9

Please sign in to comment.