You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Complete technical documentation for Fully Homomorphic Encryption integration in FHEIGHT. This guide follows the game flow from start to finish - someone reading this should understand exactly how the system works step by step.
Session Wallet Ready
|
|
v
+------------------+
| Create fhevmjs |
| Instance |
+------------------+
|
|
v
+------------------+
| Fetch Network |
| FHE Public Key |
+------------------+
|
|
v
+------------------------+
| Generate Reencryption |
| Keypair (for Gateway) |
| fhevmjs.generateKeypair|
+------------------------+
|
|
v
+------------------------+
| Sign Keypair with |
| Session Wallet (EIP712)|
+------------------------+
|
|
v
+------------------+
| FHE Ready |
+------------------+
Reencryption Keypair: This is NOT the session wallet keypair. It's a separate keypair generated by fhevmjs specifically for Gateway communication. The Gateway uses this to verify that the requesting address is authorized to decrypt the value.
FHE Operations Used
Operation
Function
Purpose
Random Generation
FHE.randEuint8()
Generate encrypted random 0-255
External Input
FHE.fromExternal()
Accept client-encrypted value
Self Permission
FHE.allowThis()
Contract can use value
User Permission
FHE.allow()
Address can decrypt value
Public Decrypt
FHE.makePubliclyDecryptable()
Gateway can decrypt
Handle Convert
FHE.toBytes32()
Convert for verification
Proof Verify
FHE.checkSignatures()
Verify decryption proof
5. CardRegistry Integration
CardRegistry stores all card metadata on-chain, ensuring card attributes cannot be tampered with.
// Server and Client use same SDK for card lookupvarcardPool=SDK.GameSession.getCardCaches().getCardSet(cardSetId)// Core, Expansion, etc..getRarity(rarityType)// Common, Rare, Epic, Legendary.getIsCollectible(true).getIsPrismatic(false).getCardIds();// Map FHE random to cardvarselectedIndex=randomValue%cardPool.length;varcardId=cardPool[selectedIndex];
5.5 GameSession Contract Code
The GameSession contract handles FHE random generation for card draws.
Contract Structure (GameSession.sol)
// SPDX-License-Identifier: MITpragma solidity^0.8.24;
import { FHE, euint8 } from"@fhevm/solidity/lib/FHE.sol";
import { ZamaEthereumConfig } from"@fhevm/solidity/config/ZamaConfig.sol";
contractGameSessionisZamaEthereumConfig {
uint8public constant DECK_SIZE =40;
uint8public constant INITIAL_HAND_SIZE =5;
struct Game {
address player;
uint8 currentTurn;
uint8 revealedCount;
bool isActive;
uint256 createdAt;
}
// Games mappingmapping(uint256=> Game) public games;
// Draw indices - encrypted random values (FHE encrypted)// IMPORTANT: euint8[40] array type doesn't work with FHE!// Must use nested mapping: gameId => index => euint8mapping(uint256=>mapping(uint8=> euint8)) private drawIndices;
// Revealed values - verified clear indicesmapping(uint256=>uint8[]) public revealedValues;
}
Game Creation with FHE Random Generation
// GameSession.sol - createSinglePlayerGame()function createSinglePlayerGame(uint256gameId) external {
require(games[gameId].player ==address(0), "Game exists");
games[gameId] =Game({
player: msg.sender,
currentTurn: 0,
revealedCount: 0,
isActive: true,
createdAt: block.timestamp
});
// Generate 40 encrypted random indices (real FHE)for (uint8 i =0; i < DECK_SIZE; i++) {
euint8 encryptedIndex = FHE.randEuint8();
// CRITICAL: Allow contract to read this value
FHE.allowThis(encryptedIndex);
// Store to storage
drawIndices[gameId][i] = encryptedIndex;
// Make publicly decryptable for client SDK
FHE.makePubliclyDecryptable(encryptedIndex);
}
emitGameCreated(gameId, msg.sender);
}
// Both client and server use IDENTICAL algorithmfunctioncalculateCardsFromIndices(deck,indices){varremaining=deck.slice();// Copy of deckvarcards=[];for(vari=0;i<indices.length;i++){varpos=indices[i]%remaining.length;cards.push(remaining[pos]);remaining.splice(pos,1);// Remove from remaining}return{drawnCards: cards,remainingDeck: remaining};}
Client Initial Hand Reveal Code (fheGameSession.js)
// fheGameSession.js - revealInitialHand()FHEGameSession.prototype.revealInitialHand=function(){varself=this;returnnewPromise(function(resolve,reject){if(self.gameId===null){reject(newError('Not in a game'));return;}// CHECK: If we have cached data, use retryNotifyServer insteadif(self._cachedInitialHandData){Logger.module('FHE_GAME').log('Cached hand found, skipping blockchain');self.retryNotifyServer().then(resolve).catch(reject);return;}Logger.module('FHE_GAME').log('=== REVEAL INITIAL HAND ===');// Wait for ZAMA ACL indexing (10 seconds on Sepolia)varwaitForACL=newPromise(function(res){setTimeout(res,10000);});waitForACL.then(function(){// Step 1: Check allowed revealsreturnself._getAllowedReveals();}).then(function(allowed){if(allowed<INITIAL_HAND_SIZE){thrownewError('Not enough allowed reveals');}// Step 2: Get draw handlesreturnself._getDrawHandles(INITIAL_HAND_SIZE);}).then(function(handles){// Step 3: Public decrypt via KMSreturnself._publicDecrypt(handles);}).then(function(result){// Step 4: Store revealed indicesself.revealedIndices=result.clearIndices.slice();// Step 5: Submit reveal batch TXreturnself._revealDrawBatch(result.clearIndices,result.abiEncodedClearValues,result.proof);}).then(function(){// Step 6: Calculate cards locallyself.myHand=self._calculateCards(self.revealedIndices);// Cache for retryself._cachedInitialHandData={hand: self.myHand.slice(),revealedIndices: self.revealedIndices.slice()};// Step 7: Notify serverreturnself._notifyServerInitialHand();}).then(function(serverCardIndices){self._cachedInitialHandData=null;// Clear cache on successresolve({cardIds: self.myHand.slice(),cardIndices: serverCardIndices});}).catch(reject);});};
Client Public Decrypt Code (fheGameSession.js)
// fheGameSession.js - _publicDecrypt()FHEGameSession.prototype._publicDecrypt=function(handles){varself=this;returnnewPromise(function(resolve,reject){// Convert handles to hex stringsvarhandleStrings=handles.map(function(h){if(typeofh==='bigint'){return'0x'+h.toString(16).padStart(64,'0');}elseif(h._hex){returnh._hex;}returnh.toString();});// Get FHEVM SDK instanceself._getFhevmInstance().then(function(instance){// Call publicDecrypt on Zama Gatewayreturninstance.publicDecrypt(handleStrings);}).then(function(result){// SDK returns clearValues as object map, not array!// Format: { '0xhandle1': value1, '0xhandle2': value2, ... }varclearIndices=handleStrings.map(function(h){varvalue=result.clearValues[h];if(typeofvalue==='bigint'){returnNumber(value%BigInt(256));}returnNumber(value)%256;});resolve({clearIndices: clearIndices,abiEncodedClearValues: result.abiEncodedClearValues||'0x',proof: result.decryptionProof||'0x'});}).catch(reject);});};
Client Game Creation Code (fheGameSession.js)
// fheGameSession.js - createSinglePlayerGame()FHEGameSession.prototype.createSinglePlayerGame=function(gameId,deck){varself=this;returnnewPromise(function(resolve,reject){if(!self.contract){reject(newError('Contract not connected'));return;}// Store deck for card calculationself.deck=deck.slice();self.remainingDeck=deck.slice();self.myHand=[];self.revealedIndices=[];self.gameId=gameId;// Encode TXvariface=newethers.utils.Interface(GAME_SESSION_ABI);vardata=iface.encodeFunctionData('createSinglePlayerGame',[gameId]);// Send TX via session walletself.sessionWallet.signTransaction({to: self.contractAddress,data: data,gasLimit: '0x7A1200'// 8M gas (40x FHE.rand)}).then(function(txResponse){returntxResponse.wait();}).then(function(receipt){if(receipt.status===0){thrownewError('Transaction reverted');}self.blockchainGameId=gameId;resolve(self.gameId);}).catch(reject);});};
Player2 receives turn before Player1 finishes FHE decrypt
Hold StartTurnAction until decrypt complete
stepCount desync between client and server
Subtract 1 from stepCount while pending
Player2 plays before game state is consistent
Emit StartTurnAction only after verification
Pending Step Logic
# game.coffee - onStep()if action instanceofSDK.EndTurnAction# Mark ending player as "drawing"endingPlayerFhe=getFHEStateForPlayer(gameId, action.getOwnerId())
ifendingPlayerFhe?.enabledendingPlayerFhe.turnDrawComplete=falseif action instanceofSDK.StartTurnAction# If ending player hasn't finished FHE draw, hold this stepifendingPlayerFhe?.enabledandnotendingPlayerFhe.turnDrawCompletegame.pendingStartTurnStep= stepEventData
return# DO NOT emit# game.coffee - onFHECardDrawn()fheState.turnDrawComplete=trueifgame.pendingStartTurnStep?emitGameEvent(null, gameId, game.pendingStartTurnStep)
game.pendingStartTurnStep=nullrestartTurnTimer(gameId)
9. Replace Card and Skill-Based Card Draw
Card abilities that draw or replace cards also use FHE to ensure fair random selection.
# game.coffee - onFHEInitialHandRevealed()onFHEInitialHandRevealed= (requestData) ->socket=@gameId=requestData.gameIdplayerId=@.playerIdfheState=getFHEStateForPlayer(gameId, playerId)
if!fheState?.enabledsocket.emit"fhe_initial_hand_revealed_response", { error:"FHE not enabled" }
returnblockchainGameId=fheState.blockchainGameIdfheDeckOrder=getFHEDeckOrderForPlayer(gameId, playerId)
network=fheState.networkor'sepolia'INITIAL_HAND_SIZE=5# BLOCKCHAIN VERIFICATION - Never trust client!BlockchainModule.verifyAndCalculateCards(network, blockchainGameId, fheDeckOrder, INITIAL_HAND_SIZE)
.then (result) ->if!result.verifiedsocket.emit"fhe_initial_hand_revealed_response", { error:"Verification failed" }
returnverifiedCards=result.cardscardIndicesForClient= []
player=game.session.getPlayerById(playerId)
playerDeck=player.getDeck()
drawPile=playerDeck.getDrawPile()
# Apply verified cards to handfor cardId, handSlotIndex in verifiedCards
for i in [0...drawPile.length]
cardIndex= drawPile[i]
card=game.session.getCardByIndex(cardIndex)
if card?andcard.getId() == cardId
game.session.applyCardToHand(playerDeck, cardIndex, card, handSlotIndex)
cardIndicesForClient.push(cardIndex)
break# Update FHE statefheState.revealedCount=verifiedCards.lengthfheState.initialHandRevealComplete=truesocket.emit"fhe_initial_hand_revealed_response", {
success:truecardIndices: cardIndicesForClient
}
# Start timer if all FHE players readyif allFheDone
restartTurnTimer(gameId)
Server Card Draw Verification (game.coffee)
# game.coffee - onFHECardDrawn()onFHECardDrawn= (requestData) ->socket=@gameId=requestData.gameIdplayerId=@.playerIdfheState=getFHEStateForPlayer(gameId, playerId)
blockchainGameId=fheState.blockchainGameIdfheDeckOrder=getFHEDeckOrderForPlayer(gameId, playerId)
network=fheState.networkor'sepolia'previousRevealCount=fheState.revealedCountor5expectedNewRevealCount= previousRevealCount +1# BLOCKCHAIN VERIFICATION - Never trust client!BlockchainModule.getVerifiedDrawOrder(network, blockchainGameId)
.then (indices) ->ifindices.length< expectedNewRevealCount
socket.emit"fhe_card_drawn_response", { error:"Reveal not yet on blockchain" }
return# Calculate ALL cards using full indices arrayresult=BlockchainModule.calculateCardsFromIndices(fheDeckOrder, indices)
# The last drawn card is the new onecardId=result.drawnCards[result.drawnCards.length-1]
player=game.session.getPlayerById(playerId)
playerDeck=player.getDeck()
drawPile=playerDeck.getDrawPile()
# Find and apply the cardfor i in [0...drawPile.length]
cardIndex= drawPile[i]
card=game.session.getCardByIndex(cardIndex)
if card?andcard.getId() == cardId
handSlotIndex=playerDeck.getNumCardsInHand()
if handSlotIndex <6# Max hand sizegame.session.applyCardToHand(playerDeck, cardIndex, card, handSlotIndex)
appliedCardIndex= cardIndex
elsecardBurned=true# Hand fullbreak# Update statefheState.revealedCount=indices.lengthfheState.turnDrawComplete=true# Emit pending StartTurnAction if waitingifgame.pendingStartTurnStep?emitGameEvent(null, gameId, game.pendingStartTurnStep)
game.pendingStartTurnStep=nullrestartTurnTimer(gameId)
socket.emit"fhe_card_drawn_response", {
success:truecardIndex: appliedCardIndex
burned: cardBurned
}
Server Blockchain Module (blockchain.coffee)
# blockchain.coffee - verifyAndCalculateCards()verifyAndCalculateCards= (network, blockchainGameId, fheDeckOrder, expectedCount) ->getVerifiedDrawOrder(network, blockchainGameId)
.then (indices) ->ifindices.length< expectedCount
return { verified:false, error:"Not enough reveals" }
# Use only first expectedCount indicesusedIndices=indices.slice(0, expectedCount)
# Calculate cards using same algorithm as clientresult=calculateCardsFromIndices(fheDeckOrder, usedIndices)
return {
verified:truecards:result.drawnCardsindices: usedIndices
remainingDeck:result.remainingDeck
}
# blockchain.coffee - getVerifiedDrawOrder()getVerifiedDrawOrder= (network, gameId) ->provider=getProvider(network)
contract=newethers.Contract(GAME_SESSION_ADDRESS, GAME_SESSION_ABI, provider)
contract.getVerifiedDrawOrder(gameId)
.then (indices) -># Convert BigNumber array to regular numbersreturnindices.map((idx) ->idx.toNumber())
12. Boss Battle Integration
Boss Battle uses the same FHE system as Single Player with different AI configuration.
Difference Table
Aspect
Single Player
Boss Battle
Endpoint
/api/me/games/single_player
/api/me/games/boss_battle
AI Difficulty
Dynamic (win count)
Fixed (1.0 - max)
AI Deck
Random
Boss-specific (GameSetups)
GameType
SDK.GameType.SinglePlayer
SDK.GameType.BossBattle
Button Text
"Encrypt & Fight"
"BOSS FHEIGHT"
Boss Battle FHE Flow
deck_select_boss_battle.js: getConfirmSelectionEvent()
|
|-- return EVENTS.start_boss_battle
|
v
application.js: _startBossBattleGame()
|
|-- if (CONFIG.fheEnabled)
| _startBossBattleGameFHE()
|
v
_startBossBattleGameFHE():
|
|-- Wallet.connect()
|-- FHE.GameMode.initialize()
|-- createSinglePlayerGame() // SAME blockchain function
|-- POST /api/me/games/boss_battle
| { fhe_enabled: true, fhe_game_id: gameId }
|
v
Server:
|-- gameSetupOptions.fheEnabled = true
|-- createSinglePlayerGame(..., gameSetupOptions)
13. Marble System (Booster Packs)
Marbles use FHE for provably fair pack opening with 15 random values.