// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import { FHE, euint64, ebool } from "@fhevm/solidity/lib/FHE.sol"; import { ZamaEthereumConfig } from "@fhevm/solidity/config/ZamaConfig.sol"; /** * @title SymbolicExecution * @author FHEVM Tutorial * @notice Demonstrates how FHEVM testing differs from production execution * * ============================================================================ * WHAT IS SYMBOLIC EXECUTION? * ============================================================================ * * In FHEVM development, there are TWO execution modes: * * ┌─────────────────────────────────────────────────────────────────────┐ * │ MOCK MODE (Testing) │ * ├─────────────────────────────────────────────────────────────────────┤ * │ - Used during local development and testing │ * │ - NO actual FHE encryption happens │ * │ - Handles are "symbolic" - they represent encrypted values │ * │ - Operations are simulated, not cryptographically computed │ * │ - FAST - no expensive crypto operations │ * │ - Values can be "peeked" for testing assertions │ * └─────────────────────────────────────────────────────────────────────┘ * * ┌─────────────────────────────────────────────────────────────────────┐ * │ PRODUCTION MODE (Real FHE) │ * ├─────────────────────────────────────────────────────────────────────┤ * │ - Used on testnet/mainnet │ * │ - ACTUAL FHE encryption with real cryptographic operations │ * │ - Handles reference real encrypted data in coprocessor │ * │ - Operations are cryptographically computed │ * │ - SLOW - expensive crypto operations │ * │ - Values CANNOT be peeked - true encryption │ * └─────────────────────────────────────────────────────────────────────┘ * * ============================================================================ * WHY DOES THIS MATTER? * ============================================================================ * * 1. TESTING ASSERTIONS * In mock mode, you can verify encrypted values match expectations. * In production, you can only verify through decryption requests. * * 2. GAS COSTS * Mock mode has minimal gas costs for FHE operations. * Production has significant gas costs for real crypto. * * 3. TIMING * Mock mode is instant. * Production operations take time for coprocessor. * * 4. DEBUGGING * Mock mode allows inspection of encrypted values. * Production is opaque - you can't see inside ciphertexts. * * ============================================================================ * WRITING TESTABLE CODE * ============================================================================ * * Good practices for code that works in both modes: * * 1. Use events for state tracking (work in both modes) * 2. Design for async decryption (production requirement) * 3. Don't rely on peeking values (mock-only feature) * 4. Test edge cases (both modes should behave identically) * * ============================================================================ */ contract SymbolicExecution is ZamaEthereumConfig { // ========================================================================= // EVENTS // ========================================================================= /// @notice Emitted when a computation is performed event ComputationPerformed( string operation, bytes32 resultHandle ); /// @notice Emitted when decryption completes event ValueDecrypted( string name, uint64 value ); /// @notice Emitted for debugging in mock mode event DebugState( string message, bytes32 handleA, bytes32 handleB, bytes32 handleResult ); // ========================================================================= // STATE // ========================================================================= /// @notice Stored encrypted values mapping(string => euint64) private _values; /// @notice Last decrypted result uint64 public lastDecryptedValue; /// @notice Pending decryption name string private _pendingDecryptionName; // ========================================================================= // DEMONSTRATION 1: BASIC OPERATIONS (Both Modes) // ========================================================================= /** * @notice Store an encrypted value * @dev Works identically in mock and production modes. * * In MOCK mode: * - The value is stored in a simulated encrypted format * - No actual encryption happens * - The "handle" is a symbolic reference * * In PRODUCTION mode: * - The value is actually encrypted by the coprocessor * - The handle references real ciphertext * - Cryptographic operations are performed */ function storeValue(string calldata name, uint64 value) external { euint64 encrypted = FHE.asEuint64(value); FHE.allowThis(encrypted); FHE.allow(encrypted, msg.sender); _values[name] = encrypted; emit ComputationPerformed("store", euint64.unwrap(encrypted)); } /** * @notice Add two stored values * @dev The operation semantics are identical in both modes, * but the underlying implementation differs completely. */ function addValues( string calldata nameA, string calldata nameB, string calldata resultName ) external { euint64 a = _values[nameA]; euint64 b = _values[nameB]; require(euint64.unwrap(a) != bytes32(0), "Value A not found"); require(euint64.unwrap(b) != bytes32(0), "Value B not found"); // This operation: // - MOCK: Simulates addition symbolically // - PRODUCTION: Performs homomorphic addition on ciphertexts euint64 result = FHE.add(a, b); FHE.allowThis(result); FHE.allow(result, msg.sender); _values[resultName] = result; emit ComputationPerformed("add", euint64.unwrap(result)); emit DebugState( "add operation", euint64.unwrap(a), euint64.unwrap(b), euint64.unwrap(result) ); } /** * @notice Compare two values * @dev Returns an encrypted boolean in both modes. */ function compareValues( string calldata nameA, string calldata nameB ) external returns (ebool) { euint64 a = _values[nameA]; euint64 b = _values[nameB]; ebool result = FHE.gt(a, b); FHE.allowThis(result); FHE.allow(result, msg.sender); emit ComputationPerformed("compare", ebool.unwrap(result)); return result; } // ========================================================================= // DEMONSTRATION 2: DECRYPTION (Mode Differences) // ========================================================================= /** * @notice Request decryption of a value (STEP 1 of 3-step pattern) * @dev This is where mock and production modes differ most: * * MOCK MODE: * - Decryption may be "instant" (simulated) * - Values can be peeked without decryption for testing * * PRODUCTION MODE: * - Decryption is async through SDK * - Takes time for coprocessor to process * - No way to peek values * * 3-STEP ASYNC PATTERN: * 1. Call this function to mark handle for decryption * 2. Off-chain: SDK calls fhevmInstance.publicDecrypt() * 3. Call finalizeDecryption() with proof */ function requestDecryption(string calldata name) external { euint64 value = _values[name]; require(euint64.unwrap(value) != bytes32(0), "Value not found"); _pendingDecryptionName = name; // Mark handle for public decryption FHE.makePubliclyDecryptable(value); emit ComputationPerformed("decrypt_request", euint64.unwrap(value)); } /** * @notice Get handle for off-chain decryption (STEP 2 helper) * @dev Client uses this to call fhevmInstance.publicDecrypt([handle]) */ function getHandleForDecryption(string calldata name) external view returns (bytes32) { return euint64.unwrap(_values[name]); } /** * @notice Finalize decryption with proof (STEP 3) * @dev Called by client after off-chain decryption completes * * MOCK: Proof verification is simulated * PRODUCTION: Cryptographic proof is verified on-chain */ function finalizeDecryption( uint64 clearValue, bytes calldata decryptionProof ) external { // Build ciphertext array for verification bytes32[] memory cts = new bytes32[](1); cts[0] = euint64.unwrap(_values[_pendingDecryptionName]); // Build cleartext bytes for verification bytes memory cleartexts = abi.encode(clearValue); // Verify the decryption proof FHE.checkSignatures(cts, cleartexts, decryptionProof); lastDecryptedValue = clearValue; emit ValueDecrypted(_pendingDecryptionName, clearValue); _pendingDecryptionName = ""; } // ========================================================================= // DEMONSTRATION 3: CONDITIONAL LOGIC (Both Modes) // ========================================================================= /** * @notice Demonstrate FHE.select (encrypted conditional) * @dev This is the FHE equivalent of: * result = condition ? valueIfTrue : valueIfFalse * * The condition, true value, and false value are ALL encrypted. * The result is also encrypted. * * MOCK: Simulates the selection * PRODUCTION: Homomorphic conditional evaluation */ function conditionalSelect( string calldata conditionName, string calldata trueName, string calldata falseName, string calldata resultName ) external { euint64 trueVal = _values[trueName]; euint64 falseVal = _values[falseName]; // Create condition: is trueVal > falseVal? ebool condition = FHE.gt(trueVal, falseVal); // Select based on encrypted condition // If condition is true, pick trueVal; else pick falseVal euint64 result = FHE.select(condition, trueVal, falseVal); FHE.allowThis(result); FHE.allow(result, msg.sender); _values[resultName] = result; emit ComputationPerformed("select", euint64.unwrap(result)); } // ========================================================================= // DEMONSTRATION 4: RANDOM NUMBERS (Mode Differences) // ========================================================================= /** * @notice Generate a random encrypted number * @dev Random number generation differs between modes: * * MOCK MODE: * - Uses a deterministic pseudo-random generator * - Same seed produces same "random" values * - Useful for reproducible tests * * PRODUCTION MODE: * - Uses cryptographically secure randomness * - Truly unpredictable values * - Non-reproducible * * TESTING TIP: Don't rely on specific random values in tests. * Test the logic around randomness, not the values themselves. */ function generateRandom(string calldata name) external { euint64 random = FHE.randEuint64(); FHE.allowThis(random); FHE.allow(random, msg.sender); _values[name] = random; emit ComputationPerformed("random", euint64.unwrap(random)); } // ========================================================================= // DEMONSTRATION 5: GAS CONSIDERATIONS // ========================================================================= /** * @notice Perform multiple operations to demonstrate gas differences * @dev In MOCK mode, gas costs are minimal for FHE operations. * In PRODUCTION mode, each operation has significant gas cost. * * Operation approximate gas costs (production): * - FHE.asEuint64: ~100k gas * - FHE.add: ~150k gas * - FHE.mul: ~200k gas * - FHE.eq/gt/lt: ~150k gas * - FHE.select: ~180k gas * * These are estimates and vary by network conditions. * * TESTING TIP: Gas estimates from mock mode are NOT accurate * for production. Always test on testnet for real gas costs. */ function multipleOperations( uint64 a, uint64 b, uint64 c ) external returns (uint256 gasUsed) { uint256 startGas = gasleft(); euint64 encA = FHE.asEuint64(a); euint64 encB = FHE.asEuint64(b); euint64 encC = FHE.asEuint64(c); // Chain of operations euint64 sum = FHE.add(encA, encB); // a + b euint64 product = FHE.mul(sum, encC); // (a + b) * c ebool isLarge = FHE.gt(product, encA); // result > a? euint64 result = FHE.select(isLarge, product, encA); FHE.allowThis(result); FHE.allow(result, msg.sender); _values["multiOp"] = result; gasUsed = startGas - gasleft(); emit ComputationPerformed("multiOp", euint64.unwrap(result)); } // ========================================================================= // UTILITIES // ========================================================================= /** * @notice Check if a value exists */ function hasValue(string calldata name) external view returns (bool) { return euint64.unwrap(_values[name]) != bytes32(0); } /** * @notice Get raw handle for a value */ function getRawHandle(string calldata name) external view returns (bytes32) { return euint64.unwrap(_values[name]); } }