Handle vs Value
🟢 Beginner | 🔑 Understanding Handles
Understanding that FHE handles are references, not values
Overview
This educational template demonstrates the critical difference between handles and values in FHEVM. Handles are 256-bit references to encrypted data, not copies of values. Key concepts covered: assignment creates aliases (not copies), handle creation is deterministic (same value = same handle), operations create new handles (even for same resulting value), and the difference between raw handle comparison vs encrypted value comparison (FHE.eq). Essential knowledge for avoiding common bugs in FHE development.
Quick Start
# Create new project from this template
npx labz create handle-vs-value my-project
# Navigate and install
cd my-project
npm install
# Run tests
npx hardhat testContract
// 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 HandleVsValue
* @author FHEVM Tutorial
* @notice Demonstrates that FHE handles are REFERENCES, not values
*
* ============================================================================
* HANDLES ARE NOT VALUES
* ============================================================================
*
* In regular Solidity:
*
* uint256 a = 100;
* uint256 b = a; // b is a COPY of a's value
* a = 200; // changing a doesn't affect b
* // a == 200, b == 100
*
* In FHEVM:
*
* euint64 a = FHE.asEuint64(100);
* euint64 b = a; // b is the SAME HANDLE as a (not a copy!)
* // Both a and b point to the same encrypted data
*
* ============================================================================
* HANDLE DETERMINISM
* ============================================================================
*
* Handles are DETERMINISTIC - the same operation on the same inputs
* produces the same handle:
*
* euint64 x = FHE.asEuint64(100);
* euint64 y = FHE.asEuint64(100);
* // x and y are the SAME HANDLE!
*
* This is because the coprocessor uses deterministic encryption based on
* the contract address, operation, and inputs.
*
* ============================================================================
* WHY DOES THIS MATTER?
* ============================================================================
*
* 1. STORAGE EFFICIENCY
* Same encrypted value = same storage slot reference
*
* 2. PERMISSION SHARING
* If you allow access to handle A, and B == A, then B is also accessible
*
* 3. EQUALITY CHECKING
* You can check if two handles reference the same encrypted data
* by comparing the raw 256-bit values
*
* 4. AVOIDING CONFUSION
* Don't expect "copying" a handle to create a new encrypted value
*
* ============================================================================
*/
contract HandleVsValue is ZamaEthereumConfig {
// =========================================================================
// EVENTS
// =========================================================================
/// @notice Logs handle values for educational comparison
event HandleComparison(
string description,
bytes32 handleA,
bytes32 handleB,
bool areSameHandle
);
/// @notice Logs when handles are created
event HandleCreated(string name, bytes32 rawHandle);
// =========================================================================
// STATE
// =========================================================================
euint64 public storedHandleA;
euint64 public storedHandleB;
euint64 public storedHandleC;
// =========================================================================
// DEMONSTRATION 1: ASSIGNMENT IS NOT COPYING
// =========================================================================
/**
* @notice Demonstrates that assigning handles doesn't create copies
* @dev When you write `b = a`, both variables reference the SAME handle.
*
* This is different from regular integers where assignment copies values.
*
* IMPORTANT: This means if you have permission on handle A, and B = A,
* you automatically have permission on B (because it's the same handle).
*/
function demo_assignmentIsNotCopy() external {
// Create a handle
euint64 handleA = FHE.asEuint64(100);
FHE.allowThis(handleA);
// "Copy" the handle - but this is NOT a copy!
euint64 handleB = handleA;
// Get raw handle values
bytes32 rawA = euint64.unwrap(handleA);
bytes32 rawB = euint64.unwrap(handleB);
// They are IDENTICAL - same handle
bool areSame = (rawA == rawB);
emit HandleCreated("handleA", rawA);
emit HandleCreated("handleB (assigned from A)", rawB);
emit HandleComparison(
"Assignment: handleB = handleA",
rawA,
rawB,
areSame // This will be TRUE
);
// Store for external verification
storedHandleA = handleA;
storedHandleB = handleB;
}
/**
* @notice Verify that stored handles are the same
* @return areSame Whether the two handles are identical
*/
function verifyAssignmentDemo() external view returns (bool areSame) {
return euint64.unwrap(storedHandleA) == euint64.unwrap(storedHandleB);
}
// =========================================================================
// DEMONSTRATION 2: DETERMINISM - SAME VALUE = SAME HANDLE
// =========================================================================
/**
* @notice Demonstrates that creating the same value twice gives the same handle
* @dev The coprocessor uses deterministic encryption. Same input + same
* context = same ciphertext = same handle.
*
* This is a key property for efficiency and reasoning about encrypted data.
*/
function demo_determinism() external {
// Create two handles from the same value
euint64 handleA = FHE.asEuint64(42);
euint64 handleB = FHE.asEuint64(42);
FHE.allowThis(handleA);
FHE.allowThis(handleB);
bytes32 rawA = euint64.unwrap(handleA);
bytes32 rawB = euint64.unwrap(handleB);
// They are the SAME handle!
bool areSame = (rawA == rawB);
emit HandleCreated("handleA = FHE.asEuint64(42)", rawA);
emit HandleCreated("handleB = FHE.asEuint64(42)", rawB);
emit HandleComparison(
"Determinism: same value twice",
rawA,
rawB,
areSame // This will be TRUE
);
storedHandleA = handleA;
storedHandleB = handleB;
}
/**
* @notice Demonstrates that different values produce different handles
* @dev This confirms that handles actually represent different encrypted values
*/
function demo_differentValues() external {
euint64 handleA = FHE.asEuint64(100);
euint64 handleB = FHE.asEuint64(200);
FHE.allowThis(handleA);
FHE.allowThis(handleB);
bytes32 rawA = euint64.unwrap(handleA);
bytes32 rawB = euint64.unwrap(handleB);
// They are DIFFERENT handles
bool areSame = (rawA == rawB);
emit HandleCreated("handleA = FHE.asEuint64(100)", rawA);
emit HandleCreated("handleB = FHE.asEuint64(200)", rawB);
emit HandleComparison(
"Different values: 100 vs 200",
rawA,
rawB,
areSame // This will be FALSE
);
storedHandleA = handleA;
storedHandleB = handleB;
}
// =========================================================================
// DEMONSTRATION 3: OPERATIONS CREATE NEW HANDLES
// =========================================================================
/**
* @notice Demonstrates that operations create NEW handles
* @dev Even if the result equals an existing value, the handle is different
* because operations produce unique handles based on their inputs.
*
* FHE.asEuint64(100) --> Handle X
* FHE.add(FHE.asEuint64(50), FHE.asEuint64(50)) --> Handle Y
*
* Even though both encrypt "100", X != Y because they came from
* different operations.
*/
function demo_operationsCreateNewHandles() external {
// Create 100 directly
euint64 handleDirect = FHE.asEuint64(100);
FHE.allowThis(handleDirect);
// Create 100 via addition: 50 + 50
euint64 fifty1 = FHE.asEuint64(50);
euint64 fifty2 = FHE.asEuint64(50);
euint64 handleFromAdd = FHE.add(fifty1, fifty2);
FHE.allowThis(handleFromAdd);
bytes32 rawDirect = euint64.unwrap(handleDirect);
bytes32 rawFromAdd = euint64.unwrap(handleFromAdd);
// They are DIFFERENT handles (even though both represent 100)
bool areSame = (rawDirect == rawFromAdd);
emit HandleCreated("handleDirect = FHE.asEuint64(100)", rawDirect);
emit HandleCreated("handleFromAdd = 50 + 50", rawFromAdd);
emit HandleComparison(
"Direct vs Operation: both are 100",
rawDirect,
rawFromAdd,
areSame // This will be FALSE
);
storedHandleA = handleDirect;
storedHandleB = handleFromAdd;
}
// =========================================================================
// DEMONSTRATION 4: OPERATION DETERMINISM
// =========================================================================
/**
* @notice Demonstrates that the same operation on same inputs = same handle
* @dev Operations are also deterministic. If you add A + B twice,
* you get the same resulting handle.
*/
function demo_operationDeterminism() external {
euint64 baseA = FHE.asEuint64(10);
euint64 baseB = FHE.asEuint64(20);
FHE.allowThis(baseA);
FHE.allowThis(baseB);
// Perform the same operation twice
euint64 result1 = FHE.add(baseA, baseB);
euint64 result2 = FHE.add(baseA, baseB);
FHE.allowThis(result1);
FHE.allowThis(result2);
bytes32 raw1 = euint64.unwrap(result1);
bytes32 raw2 = euint64.unwrap(result2);
// Same operation, same inputs = same handle
bool areSame = (raw1 == raw2);
emit HandleCreated("result1 = A + B", raw1);
emit HandleCreated("result2 = A + B (same operation)", raw2);
emit HandleComparison(
"Operation determinism: A+B done twice",
raw1,
raw2,
areSame // This will be TRUE
);
storedHandleA = result1;
storedHandleB = result2;
}
// =========================================================================
// DEMONSTRATION 5: THREE-WAY COMPARISON
// =========================================================================
/**
* @notice Compare three handles from different sources
* @dev Shows the full picture of handle identity
*/
function demo_threeWayComparison() external {
// Three ways to represent "30"
euint64 directThirty = FHE.asEuint64(30);
euint64 anotherDirectThirty = FHE.asEuint64(30);
euint64 addedThirty = FHE.add(FHE.asEuint64(10), FHE.asEuint64(20));
FHE.allowThis(directThirty);
FHE.allowThis(anotherDirectThirty);
FHE.allowThis(addedThirty);
bytes32 rawDirect1 = euint64.unwrap(directThirty);
bytes32 rawDirect2 = euint64.unwrap(anotherDirectThirty);
bytes32 rawAdded = euint64.unwrap(addedThirty);
emit HandleCreated("directThirty", rawDirect1);
emit HandleCreated("anotherDirectThirty", rawDirect2);
emit HandleCreated("addedThirty (10+20)", rawAdded);
// Direct creations are the same
emit HandleComparison(
"Direct vs Direct",
rawDirect1,
rawDirect2,
rawDirect1 == rawDirect2 // TRUE - same deterministic creation
);
// Direct vs Operation are different
emit HandleComparison(
"Direct vs Added",
rawDirect1,
rawAdded,
rawDirect1 == rawAdded // FALSE - different origins
);
storedHandleA = directThirty;
storedHandleB = anotherDirectThirty;
storedHandleC = addedThirty;
}
// =========================================================================
// UTILITIES
// =========================================================================
/**
* @notice Get raw handle values for external comparison
*/
function getRawHandles() external view returns (bytes32 a, bytes32 b, bytes32 c) {
return (
euint64.unwrap(storedHandleA),
euint64.unwrap(storedHandleB),
euint64.unwrap(storedHandleC)
);
}
/**
* @notice Check if two stored handles are identical
*/
function areHandlesIdentical() external view returns (bool abSame, bool acSame, bool bcSame) {
bytes32 a = euint64.unwrap(storedHandleA);
bytes32 b = euint64.unwrap(storedHandleB);
bytes32 c = euint64.unwrap(storedHandleC);
return (a == b, a == c, b == c);
}
/**
* @notice Demonstrates that comparing handles is NOT the same as comparing values
* @dev This creates an encrypted comparison - the result is also encrypted!
*
* To compare if two HANDLES represent the same VALUE (without knowing
* what the value is), you need FHE.eq() which returns ebool.
*/
function demo_encryptedComparison() external returns (ebool) {
// Two handles that represent the same value
euint64 handleA = FHE.asEuint64(50);
euint64 handleB = FHE.add(FHE.asEuint64(25), FHE.asEuint64(25));
FHE.allowThis(handleA);
FHE.allowThis(handleB);
// Their RAW handles are DIFFERENT (different origins)
// But their ENCRYPTED VALUES are EQUAL
// This is an ENCRYPTED comparison - result is encrypted
ebool areValuesEqual = FHE.eq(handleA, handleB);
FHE.allowThis(areValuesEqual);
FHE.allow(areValuesEqual, msg.sender);
// areValuesEqual is a HANDLE to an encrypted boolean (true)
// You'd need to decrypt it to see the result
return areValuesEqual;
}
}
Code Explanation
Assignment Not Copy
Demonstrates that b = a makes both point to the same handle, not a copy.
Lines 78-110
Determinism
Shows that same value encrypted twice produces the same handle (deterministic).
Lines 118-162
Operations New Handles
Demonstrates that FHE operations create NEW handles, different from direct creation.
Lines 168-210
Operation Determinism
Shows that same operation on same inputs = same handle (operation determinism).
Lines 216-250
Encrypted Comparison
Compares raw handle equality vs FHE.eq() value equality. Different handles can have equal values.
Lines 290-320
FHE Operations Used
FHE.FHE.asEuint64()FHE.FHE.add()FHE.FHE.eq()FHE.FHE.allow()FHE.FHE.allowThis()
FHE Types Used
euint64ebool
Tags
handles reference value determinism comparison educational
Related Examples
Prerequisites
Before this example, you should understand:
Next Steps
After this example, check out:
Generated with Lab-Z
Last updated
