Handle Observatory

🔴 Advanced | 🔑 Understanding Handles

Advanced handle tracking and registry system for debugging and auditing

Overview

The Handle Observatory is a comprehensive system for tracking, debugging, and auditing FHE handles. It provides: (1) Registry - every handle is registered with metadata including name, origin, creator, and timestamp; (2) Genealogy - track parent-child relationships between handles to understand computation flow; (3) Permissions - complete audit trail of all permission grants with timestamps; (4) Lifecycle - monitor handle states (ACTIVE, DECRYPTING, DECRYPTED, ARCHIVED). Essential for debugging complex FHE computations, compliance auditing, and understanding encrypted data flow.

Quick Start

# Create new project from this template
npx labz create observatory my-project

# Navigate and install
cd my-project
npm install

# Run tests
npx hardhat test

Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import { FHE, euint64, ebool, externalEuint64 } from "@fhevm/solidity/lib/FHE.sol";
import { ZamaEthereumConfig } from "@fhevm/solidity/config/ZamaConfig.sol";

/**
 * @title HandleObservatory
 * @author FHEVM Tutorial
 * @notice Advanced handle tracking and registry system for debugging and auditing
 *
 * ============================================================================
 *                         HANDLE OBSERVATORY
 * ============================================================================
 *
 * This contract provides a comprehensive system for tracking FHE handles:
 *
 *   1. REGISTRY - Every handle is registered with metadata
 *   2. GENEALOGY - Track parent-child relationships between handles
 *   3. PERMISSIONS - Audit trail of all permission grants
 *   4. LIFECYCLE - Monitor handle states (active, decrypted, etc.)
 *
 * ============================================================================
 *                            USE CASES
 * ============================================================================
 *
 *   - DEBUGGING: Trace which operation created which handle
 *   - AUDITING: See who has permission on which handles
 *   - OPTIMIZATION: Identify duplicate handle creation
 *   - COMPLIANCE: Full audit trail of encrypted data lifecycle
 *
 * ============================================================================
 *                        HANDLE METADATA
 * ============================================================================
 *
 *   struct HandleInfo {
 *       uint256 rawHandle;      // The 256-bit handle value
 *       string name;            // Human-readable identifier
 *       HandleOrigin origin;    // How it was created
 *       uint256 parentHandle;   // Parent (if from operation)
 *       uint256 createdAt;      // Block timestamp
 *       HandleState state;      // Current state
 *   }
 *
 * ============================================================================
 */
contract HandleObservatory is ZamaEthereumConfig {

    // =========================================================================
    //                              ENUMS
    // =========================================================================

    /// @notice How a handle was created
    enum HandleOrigin {
        PLAINTEXT,      // FHE.asEuint64(value)
        USER_INPUT,     // FHE.fromExternal(input, proof)
        OPERATION,      // FHE.add(), FHE.mul(), etc.
        RANDOM          // FHE.randEuint64()
    }

    /// @notice Current state of a handle
    enum HandleState {
        ACTIVE,         // Handle is live and usable
        DECRYPTING,     // Decryption requested, waiting for callback
        DECRYPTED,      // Value has been revealed
        ARCHIVED        // Marked as no longer needed
    }

    // =========================================================================
    //                              STRUCTS
    // =========================================================================

    /// @notice Complete metadata for a registered handle
    struct HandleInfo {
        uint256 rawHandle;          // The 256-bit handle value
        string name;                // Human-readable identifier
        HandleOrigin origin;        // How it was created
        uint256[] parentHandles;    // Parent handles (for operations)
        string operationType;       // "add", "mul", etc. (if from operation)
        uint256 createdAt;          // Block timestamp
        address creator;            // Who created it
        HandleState state;          // Current lifecycle state
        uint64 decryptedValue;      // Value after decryption (0 if not decrypted)
    }

    /// @notice Permission grant record
    struct PermissionRecord {
        uint256 rawHandle;          // Which handle
        address grantee;            // Who received permission
        string permissionType;      // "allow", "allowTransient", "allowThis"
        uint256 grantedAt;          // When it was granted
        address grantedBy;          // Who granted it
    }

    // =========================================================================
    //                              EVENTS
    // =========================================================================

    /// @notice Emitted when a new handle is registered
    event HandleRegistered(
        uint256 indexed rawHandle,
        string name,
        HandleOrigin origin,
        address indexed creator
    );

    /// @notice Emitted when a child handle is created from parents
    event HandleDerived(
        uint256 indexed childHandle,
        uint256[] parentHandles,
        string operation
    );

    /// @notice Emitted when permission is granted
    event PermissionRecorded(
        uint256 indexed rawHandle,
        address indexed grantee,
        string permissionType
    );

    /// @notice Emitted when handle state changes
    event HandleStateChanged(
        uint256 indexed rawHandle,
        HandleState oldState,
        HandleState newState
    );

    /// @notice Emitted when a handle is decrypted
    event HandleDecrypted(
        uint256 indexed rawHandle,
        uint64 value
    );

    // =========================================================================
    //                              STATE
    // =========================================================================

    /// @notice Counter for registered handles
    uint256 public handleCount;

    /// @notice All registered handles (indexed by rawHandle)
    mapping(uint256 => HandleInfo) public handleRegistry;

    /// @notice Mapping from ID (1-based) to rawHandle
    mapping(uint256 => uint256) public handleById;

    /// @notice All permission records
    PermissionRecord[] public permissionLog;

    /// @notice The actual encrypted values (stored by name for easy access)
    mapping(string => euint64) private _handles;

    // =========================================================================
    //                         HANDLE REGISTRATION
    // =========================================================================

    /**
     * @notice Create and register a handle from plaintext
     * @param value The plaintext value to encrypt
     * @param name Human-readable identifier for this handle
     * @return rawHandle The 256-bit handle value
     */
    function createFromPlaintext(
        uint64 value,
        string calldata name
    ) external returns (uint256 rawHandle) {
        // Create the encrypted handle
        euint64 handle = FHE.asEuint64(value);
        FHE.allowThis(handle);
        FHE.allow(handle, msg.sender);

        rawHandle = uint256(euint64.unwrap(handle));

        // Register it
        _registerHandle(
            rawHandle,
            name,
            HandleOrigin.PLAINTEXT,
            new uint256[](0),
            ""
        );

        // Store for later use
        _handles[name] = handle;

        // Record permission grants
        _recordPermission(rawHandle, address(this), "allowThis");
        _recordPermission(rawHandle, msg.sender, "allow");
    }

    /**
     * @notice Create and register a handle from user input
     * @param encryptedInput The encrypted value from the user
     * @param inputProof Proof of valid encryption
     * @param name Human-readable identifier
     * @return rawHandle The 256-bit handle value
     */
    function createFromUserInput(
        externalEuint64 encryptedInput,
        bytes calldata inputProof,
        string calldata name
    ) external returns (uint256 rawHandle) {
        euint64 handle = FHE.fromExternal(encryptedInput, inputProof);
        FHE.allowThis(handle);
        FHE.allow(handle, msg.sender);

        rawHandle = uint256(euint64.unwrap(handle));

        _registerHandle(
            rawHandle,
            name,
            HandleOrigin.USER_INPUT,
            new uint256[](0),
            ""
        );

        _handles[name] = handle;
        _recordPermission(rawHandle, address(this), "allowThis");
        _recordPermission(rawHandle, msg.sender, "allow");
    }

    /**
     * @notice Create and register a random handle
     * @param name Human-readable identifier
     * @return rawHandle The 256-bit handle value
     */
    function createRandom(string calldata name) external returns (uint256 rawHandle) {
        euint64 handle = FHE.randEuint64();
        FHE.allowThis(handle);
        FHE.allow(handle, msg.sender);

        rawHandle = uint256(euint64.unwrap(handle));

        _registerHandle(
            rawHandle,
            name,
            HandleOrigin.RANDOM,
            new uint256[](0),
            ""
        );

        _handles[name] = handle;
        _recordPermission(rawHandle, address(this), "allowThis");
        _recordPermission(rawHandle, msg.sender, "allow");
    }

    // =========================================================================
    //                         OPERATIONS (WITH TRACKING)
    // =========================================================================

    /**
     * @notice Add two handles and register the result
     * @param nameA Name of first operand
     * @param nameB Name of second operand
     * @param resultName Name for the result
     * @return rawHandle The 256-bit handle of the result
     */
    function operationAdd(
        string calldata nameA,
        string calldata nameB,
        string calldata resultName
    ) external returns (uint256 rawHandle) {
        euint64 handleA = _handles[nameA];
        euint64 handleB = _handles[nameB];

        require(euint64.unwrap(handleA) != bytes32(0), "Handle A not found");
        require(euint64.unwrap(handleB) != bytes32(0), "Handle B not found");

        // Perform operation
        euint64 result = FHE.add(handleA, handleB);
        FHE.allowThis(result);
        FHE.allow(result, msg.sender);

        rawHandle = uint256(euint64.unwrap(result));

        // Record parent-child relationship
        uint256[] memory parents = new uint256[](2);
        parents[0] = uint256(euint64.unwrap(handleA));
        parents[1] = uint256(euint64.unwrap(handleB));

        _registerHandle(
            rawHandle,
            resultName,
            HandleOrigin.OPERATION,
            parents,
            "add"
        );

        emit HandleDerived(rawHandle, parents, "add");

        _handles[resultName] = result;
        _recordPermission(rawHandle, address(this), "allowThis");
        _recordPermission(rawHandle, msg.sender, "allow");
    }

    /**
     * @notice Multiply two handles and register the result
     */
    function operationMul(
        string calldata nameA,
        string calldata nameB,
        string calldata resultName
    ) external returns (uint256 rawHandle) {
        euint64 handleA = _handles[nameA];
        euint64 handleB = _handles[nameB];

        require(euint64.unwrap(handleA) != bytes32(0), "Handle A not found");
        require(euint64.unwrap(handleB) != bytes32(0), "Handle B not found");

        euint64 result = FHE.mul(handleA, handleB);
        FHE.allowThis(result);
        FHE.allow(result, msg.sender);

        rawHandle = uint256(euint64.unwrap(result));

        uint256[] memory parents = new uint256[](2);
        parents[0] = uint256(euint64.unwrap(handleA));
        parents[1] = uint256(euint64.unwrap(handleB));

        _registerHandle(rawHandle, resultName, HandleOrigin.OPERATION, parents, "mul");
        emit HandleDerived(rawHandle, parents, "mul");

        _handles[resultName] = result;
        _recordPermission(rawHandle, address(this), "allowThis");
        _recordPermission(rawHandle, msg.sender, "allow");
    }

    /**
     * @notice Compare two handles (greater than)
     */
    function operationGt(
        string calldata nameA,
        string calldata nameB,
        string calldata resultName
    ) external returns (uint256 rawHandle) {
        euint64 handleA = _handles[nameA];
        euint64 handleB = _handles[nameB];

        ebool result = FHE.gt(handleA, handleB);
        FHE.allowThis(result);
        FHE.allow(result, msg.sender);

        rawHandle = uint256(ebool.unwrap(result));

        uint256[] memory parents = new uint256[](2);
        parents[0] = uint256(euint64.unwrap(handleA));
        parents[1] = uint256(euint64.unwrap(handleB));

        _registerHandle(rawHandle, resultName, HandleOrigin.OPERATION, parents, "gt");
        emit HandleDerived(rawHandle, parents, "gt");

        _recordPermission(rawHandle, address(this), "allowThis");
        _recordPermission(rawHandle, msg.sender, "allow");
    }

    // =========================================================================
    //                         PERMISSION MANAGEMENT
    // =========================================================================

    /**
     * @notice Grant permanent permission and record it
     */
    function grantPermission(string calldata name, address grantee) external {
        euint64 handle = _handles[name];
        require(euint64.unwrap(handle) != bytes32(0), "Handle not found");

        FHE.allow(handle, grantee);
        _recordPermission(uint256(euint64.unwrap(handle)), grantee, "allow");
    }

    /**
     * @notice Grant transient permission and record it
     */
    function grantTransientPermission(string calldata name, address grantee) external {
        euint64 handle = _handles[name];
        require(euint64.unwrap(handle) != bytes32(0), "Handle not found");

        FHE.allowTransient(handle, grantee);
        _recordPermission(uint256(euint64.unwrap(handle)), grantee, "allowTransient");
    }

    // =========================================================================
    //                         DECRYPTION (STATE CHANGE)
    // =========================================================================

    /**
     * @notice Request decryption of a handle (STEP 1 of 3-step pattern)
     * @param name Name of the handle to decrypt
     */
    function requestDecryption(string calldata name) external {
        euint64 handle = _handles[name];
        uint256 rawHandle = uint256(euint64.unwrap(handle));
        require(rawHandle != 0, "Handle not found");

        // Update state to DECRYPTING
        HandleState oldState = handleRegistry[rawHandle].state;
        handleRegistry[rawHandle].state = HandleState.DECRYPTING;
        emit HandleStateChanged(rawHandle, oldState, HandleState.DECRYPTING);

        // STEP 1: Mark for public decryption
        FHE.makePubliclyDecryptable(handle);
    }

    /**
     * @notice Get handle for off-chain decryption (STEP 2 helper)
     */
    function getHandleForDecryption(string calldata name) external view returns (bytes32) {
        return euint64.unwrap(_handles[name]);
    }

    /**
     * @notice Finalize decryption with proof (STEP 3)
     */
    function finalizeDecryption(
        string calldata name,
        uint64 clearValue,
        bytes calldata decryptionProof
    ) external {
        euint64 handle = _handles[name];
        uint256 rawHandle = uint256(euint64.unwrap(handle));
        require(rawHandle != 0, "Handle not found");

        // Build ciphertext array for verification
        bytes32[] memory cts = new bytes32[](1);
        cts[0] = euint64.unwrap(handle);

        // Build cleartext bytes for verification
        bytes memory cleartexts = abi.encode(clearValue);

        // Verify the decryption proof
        FHE.checkSignatures(cts, cleartexts, decryptionProof);

        // Update state to DECRYPTED
        HandleState oldState = handleRegistry[rawHandle].state;
        handleRegistry[rawHandle].state = HandleState.DECRYPTED;
        handleRegistry[rawHandle].decryptedValue = clearValue;

        emit HandleStateChanged(rawHandle, oldState, HandleState.DECRYPTED);
        emit HandleDecrypted(rawHandle, clearValue);
    }

    // =========================================================================
    //                         QUERY FUNCTIONS
    // =========================================================================

    /**
     * @notice Get complete info for a handle by name
     */
    function getHandleInfo(string calldata name) external view returns (HandleInfo memory) {
        euint64 handle = _handles[name];
        uint256 rawHandle = uint256(euint64.unwrap(handle));
        return handleRegistry[rawHandle];
    }

    /**
     * @notice Get handle info by raw value
     */
    function getHandleInfoByRaw(uint256 rawHandle) external view returns (HandleInfo memory) {
        return handleRegistry[rawHandle];
    }

    /**
     * @notice Get all permission records for a handle
     */
    function getPermissionsForHandle(uint256 rawHandle) external view returns (PermissionRecord[] memory) {
        // Count permissions for this handle
        uint256 count = 0;
        for (uint256 i = 0; i < permissionLog.length; i++) {
            if (permissionLog[i].rawHandle == rawHandle) {
                count++;
            }
        }

        // Collect them
        PermissionRecord[] memory result = new PermissionRecord[](count);
        uint256 idx = 0;
        for (uint256 i = 0; i < permissionLog.length; i++) {
            if (permissionLog[i].rawHandle == rawHandle) {
                result[idx++] = permissionLog[i];
            }
        }

        return result;
    }

    /**
     * @notice Get child handles derived from a parent
     */
    function getChildHandles(uint256 parentRawHandle) external view returns (uint256[] memory) {
        // Count children
        uint256 count = 0;
        for (uint256 i = 1; i <= handleCount; i++) {
            uint256 rawHandle = handleById[i];
            HandleInfo storage info = handleRegistry[rawHandle];
            for (uint256 j = 0; j < info.parentHandles.length; j++) {
                if (info.parentHandles[j] == parentRawHandle) {
                    count++;
                    break;
                }
            }
        }

        // Collect children
        uint256[] memory children = new uint256[](count);
        uint256 idx = 0;
        for (uint256 i = 1; i <= handleCount; i++) {
            uint256 rawHandle = handleById[i];
            HandleInfo storage info = handleRegistry[rawHandle];
            for (uint256 j = 0; j < info.parentHandles.length; j++) {
                if (info.parentHandles[j] == parentRawHandle) {
                    children[idx++] = rawHandle;
                    break;
                }
            }
        }

        return children;
    }

    /**
     * @notice Get total permission count
     */
    function getPermissionCount() external view returns (uint256) {
        return permissionLog.length;
    }

    /**
     * @notice Get raw handle by name
     */
    function getRawHandle(string calldata name) external view returns (uint256) {
        return uint256(euint64.unwrap(_handles[name]));
    }

    // =========================================================================
    //                         INTERNAL FUNCTIONS
    // =========================================================================

    function _registerHandle(
        uint256 rawHandle,
        string memory name,
        HandleOrigin origin,
        uint256[] memory parents,
        string memory operationType
    ) internal {
        handleCount++;
        handleById[handleCount] = rawHandle;

        handleRegistry[rawHandle] = HandleInfo({
            rawHandle: rawHandle,
            name: name,
            origin: origin,
            parentHandles: parents,
            operationType: operationType,
            createdAt: block.timestamp,
            creator: msg.sender,
            state: HandleState.ACTIVE,
            decryptedValue: 0
        });

        emit HandleRegistered(rawHandle, name, origin, msg.sender);
    }

    function _recordPermission(
        uint256 rawHandle,
        address grantee,
        string memory permissionType
    ) internal {
        permissionLog.push(PermissionRecord({
            rawHandle: rawHandle,
            grantee: grantee,
            permissionType: permissionType,
            grantedAt: block.timestamp,
            grantedBy: msg.sender
        }));

        emit PermissionRecorded(rawHandle, grantee, permissionType);
    }
}

Code Explanation

Registration

Register handles with metadata: name, origin, creator, timestamp. Supports plaintext, user input, and random origins.

Lines 130-195

Operations Tracking

Perform operations (add, mul, gt) while tracking parent-child relationships and operation types.

Lines 200-285

Permission Audit

Grant permissions while maintaining complete audit trail with grantee, granter, and timestamp.

Lines 290-320

Lifecycle Tracking

Track handle state transitions: ACTIVE -> DECRYPTING -> DECRYPTED. Record decrypted values.

Lines 325-380

Query Functions

Query handle info, permissions, and child handles. Build dependency graphs for debugging.

Lines 385-455

FHE Operations Used

  • FHE.FHE.asEuint64()

  • FHE.FHE.fromExternal()

  • FHE.FHE.randEuint64()

  • FHE.FHE.add()

  • FHE.FHE.mul()

  • FHE.FHE.gt()

  • FHE.FHE.allow()

  • FHE.FHE.allowThis()

  • FHE.FHE.allowTransient()

  • FHE.FHE.makePubliclyDecryptable()

  • FHE.FHE.checkSignatures()

FHE Types Used

  • euint64

  • ebool

  • externalEuint64

Tags

handles registry tracking debugging audit genealogy permissions lifecycle

Prerequisites

Before this example, you should understand:

Next Steps

After this example, check out:


Generated with Lab-Z

Last updated