diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..be96af5 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,61 @@ +name: Tests + +on: + pull_request: + branches: + - master + push: + branches: + - master + +jobs: + test-host-chain: + name: Host Chain Contract Tests + runs-on: ubuntu-latest + defaults: + run: + working-directory: contracts/internal/host-chain + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 9 + + - name: Get pnpm store directory + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('contracts/internal/host-chain/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - name: Compile contracts + run: pnpm compile + env: + # Dummy keys for hardhat config (not used - tests run on Hardhat network) + KEY: "0x0000000000000000000000000000000000000000000000000000000000000001" + KEY2: "0x0000000000000000000000000000000000000000000000000000000000000002" + AGGREGATOR_KEY: "0x0000000000000000000000000000000000000000000000000000000000000003" + + - name: Run tests + run: pnpm test + env: + KEY: "0x0000000000000000000000000000000000000000000000000000000000000001" + KEY2: "0x0000000000000000000000000000000000000000000000000000000000000002" + AGGREGATOR_KEY: "0x0000000000000000000000000000000000000000000000000000000000000003" diff --git a/CHANGELOG.md b/CHANGELOG.md index 51623bf..9eb0054 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [Unreleased] + +### Added +- `isPubliclyAllowed(uint256 ctHash)` view function on `TaskManager` to query whether a ciphertext handle has been publicly allowed (via `allowGlobal` / `allowPublic`). Delegates to `acl.globalAllowed()`. +- `FHE.isPubliclyAllowed()` typed overloads for all encrypted types (`ebool`, `euint8`, ..., `eaddress`) so contracts can query public-allow status directly via the FHE library. +- `publishDecryptResult()` and `publishDecryptResultBatch()` on TaskManager for publishing signed decrypt results on-chain +- `verifyDecryptResult()` (reverts on invalid) and `verifyDecryptResultSafe()` (returns false) for signature verification without publishing +- `decryptResultSigner` state variable and `setDecryptResultSigner()` admin function +- Typed overloads in `FHE.sol` for all encrypted types (`ebool`, `euint8`, ..., `eaddress`) +- `onlyIfEnabled` modifier on publish functions +- `LengthMismatch` custom error replacing require string in batch publish + ## v0.1.0 ### Breaking Changes diff --git a/contracts/FHE.sol b/contracts/FHE.sol index 3a4441e..15571aa 100644 --- a/contracts/FHE.sol +++ b/contracts/FHE.sol @@ -144,6 +144,26 @@ library Impl { return ITaskManager(TASK_MANAGER_ADDRESS).getDecryptResultSafe(uint256(input)); } + function publishDecryptResult(bytes32 ctHash, uint256 result, bytes memory signature) internal { + ITaskManager(TASK_MANAGER_ADDRESS).publishDecryptResult(uint256(ctHash), result, signature); + } + + function publishDecryptResultBatch(bytes32[] memory ctHashes, uint256[] memory results, bytes[] memory signatures) internal { + uint256[] memory ctHashesUint = new uint256[](ctHashes.length); + for (uint256 i = 0; i < ctHashes.length; i++) { + ctHashesUint[i] = uint256(ctHashes[i]); + } + ITaskManager(TASK_MANAGER_ADDRESS).publishDecryptResultBatch(ctHashesUint, results, signatures); + } + + function verifyDecryptResult(bytes32 ctHash, uint256 result, bytes memory signature) internal view returns (bool) { + return ITaskManager(TASK_MANAGER_ADDRESS).verifyDecryptResult(uint256(ctHash), result, signature); + } + + function verifyDecryptResultSafe(bytes32 ctHash, uint256 result, bytes memory signature) internal view returns (bool) { + return ITaskManager(TASK_MANAGER_ADDRESS).verifyDecryptResultSafe(uint256(ctHash), result, signature); + } + function not(uint8 returnType, bytes32 input) internal returns (bytes32) { return bytes32(ITaskManager(TASK_MANAGER_ADDRESS).createTask(returnType, FunctionId.not, Common.createUint256Inputs(input), new uint256[](0))); } @@ -3027,6 +3047,55 @@ library FHE { ITaskManager(TASK_MANAGER_ADDRESS).allowGlobal(uint256(eaddress.unwrap(ctHash))); } + /// @notice Grants public permission to operate on the encrypted boolean value + /// @dev Allows all accounts to access the ciphertext + /// @param ctHash The encrypted boolean value to grant public access to + function allowPublic(ebool ctHash) internal { + ITaskManager(TASK_MANAGER_ADDRESS).allowGlobal(ebool.unwrap(ctHash)); + } + + /// @notice Grants public permission to operate on the encrypted 8-bit unsigned integer + /// @dev Allows all accounts to access the ciphertext + /// @param ctHash The encrypted uint8 value to grant public access to + function allowPublic(euint8 ctHash) internal { + ITaskManager(TASK_MANAGER_ADDRESS).allowGlobal(euint8.unwrap(ctHash)); + } + + /// @notice Grants public permission to operate on the encrypted 16-bit unsigned integer + /// @dev Allows all accounts to access the ciphertext + /// @param ctHash The encrypted uint16 value to grant public access to + function allowPublic(euint16 ctHash) internal { + ITaskManager(TASK_MANAGER_ADDRESS).allowGlobal(euint16.unwrap(ctHash)); + } + + /// @notice Grants public permission to operate on the encrypted 32-bit unsigned integer + /// @dev Allows all accounts to access the ciphertext + /// @param ctHash The encrypted uint32 value to grant public access to + function allowPublic(euint32 ctHash) internal { + ITaskManager(TASK_MANAGER_ADDRESS).allowGlobal(euint32.unwrap(ctHash)); + } + + /// @notice Grants public permission to operate on the encrypted 64-bit unsigned integer + /// @dev Allows all accounts to access the ciphertext + /// @param ctHash The encrypted uint64 value to grant public access to + function allowPublic(euint64 ctHash) internal { + ITaskManager(TASK_MANAGER_ADDRESS).allowGlobal(euint64.unwrap(ctHash)); + } + + /// @notice Grants public permission to operate on the encrypted 128-bit unsigned integer + /// @dev Allows all accounts to access the ciphertext + /// @param ctHash The encrypted uint128 value to grant public access to + function allowPublic(euint128 ctHash) internal { + ITaskManager(TASK_MANAGER_ADDRESS).allowGlobal(euint128.unwrap(ctHash)); + } + + /// @notice Grants public permission to operate on the encrypted address + /// @dev Allows all accounts to access the ciphertext + /// @param ctHash The encrypted address value to grant public access to + function allowPublic(eaddress ctHash) internal { + ITaskManager(TASK_MANAGER_ADDRESS).allowGlobal(eaddress.unwrap(ctHash)); + } + /// @notice Checks if an account has permission to operate on the encrypted boolean value /// @dev Returns whether the specified account can access the ciphertext /// @param ctHash The encrypted boolean value to check access for @@ -3091,6 +3160,55 @@ library FHE { return ITaskManager(TASK_MANAGER_ADDRESS).isAllowed(uint256(eaddress.unwrap(ctHash)), account); } + /// @notice Checks if an encrypted boolean value is publicly (globally) allowed + /// @param ctHash The encrypted boolean value to check + /// @return True if the ciphertext is publicly allowed, false otherwise + function isPubliclyAllowed(ebool ctHash) internal view returns (bool) { + return ITaskManager(TASK_MANAGER_ADDRESS).isPubliclyAllowed(uint256(ebool.unwrap(ctHash))); + } + + /// @notice Checks if an encrypted 8-bit unsigned integer is publicly (globally) allowed + /// @param ctHash The encrypted uint8 value to check + /// @return True if the ciphertext is publicly allowed, false otherwise + function isPubliclyAllowed(euint8 ctHash) internal view returns (bool) { + return ITaskManager(TASK_MANAGER_ADDRESS).isPubliclyAllowed(uint256(euint8.unwrap(ctHash))); + } + + /// @notice Checks if an encrypted 16-bit unsigned integer is publicly (globally) allowed + /// @param ctHash The encrypted uint16 value to check + /// @return True if the ciphertext is publicly allowed, false otherwise + function isPubliclyAllowed(euint16 ctHash) internal view returns (bool) { + return ITaskManager(TASK_MANAGER_ADDRESS).isPubliclyAllowed(uint256(euint16.unwrap(ctHash))); + } + + /// @notice Checks if an encrypted 32-bit unsigned integer is publicly (globally) allowed + /// @param ctHash The encrypted uint32 value to check + /// @return True if the ciphertext is publicly allowed, false otherwise + function isPubliclyAllowed(euint32 ctHash) internal view returns (bool) { + return ITaskManager(TASK_MANAGER_ADDRESS).isPubliclyAllowed(uint256(euint32.unwrap(ctHash))); + } + + /// @notice Checks if an encrypted 64-bit unsigned integer is publicly (globally) allowed + /// @param ctHash The encrypted uint64 value to check + /// @return True if the ciphertext is publicly allowed, false otherwise + function isPubliclyAllowed(euint64 ctHash) internal view returns (bool) { + return ITaskManager(TASK_MANAGER_ADDRESS).isPubliclyAllowed(uint256(euint64.unwrap(ctHash))); + } + + /// @notice Checks if an encrypted 128-bit unsigned integer is publicly (globally) allowed + /// @param ctHash The encrypted uint128 value to check + /// @return True if the ciphertext is publicly allowed, false otherwise + function isPubliclyAllowed(euint128 ctHash) internal view returns (bool) { + return ITaskManager(TASK_MANAGER_ADDRESS).isPubliclyAllowed(uint256(euint128.unwrap(ctHash))); + } + + /// @notice Checks if an encrypted address is publicly (globally) allowed + /// @param ctHash The encrypted address value to check + /// @return True if the ciphertext is publicly allowed, false otherwise + function isPubliclyAllowed(eaddress ctHash) internal view returns (bool) { + return ITaskManager(TASK_MANAGER_ADDRESS).isPubliclyAllowed(uint256(eaddress.unwrap(ctHash))); + } + /// @notice Grants permission to the current contract to operate on the encrypted boolean value /// @dev Allows this contract to access the ciphertext /// @param ctHash The encrypted boolean value to grant access to @@ -3244,6 +3362,154 @@ library FHE { function allowTransient(eaddress ctHash, address account) internal { ITaskManager(TASK_MANAGER_ADDRESS).allowTransient(uint256(eaddress.unwrap(ctHash)), account); } + + // ********** PUBLISH DECRYPT RESULT ************* // + + /// @notice Publish a signed decrypt result to the chain + /// @dev Anyone with a valid signature from the decrypt network can call this + /// @param ctHash The ciphertext hash + /// @param result The decrypted plaintext value + /// @param signature The ECDSA signature from the decrypt network + function publishDecryptResult(uint256 ctHash, uint256 result, bytes memory signature) internal { + Impl.publishDecryptResult(bytes32(ctHash), result, signature); + } + + /// @notice Publish a signed decrypt result for an ebool + function publishDecryptResult(ebool input, bool result, bytes memory signature) internal { + Impl.publishDecryptResult(ebool.unwrap(input), result ? 1 : 0, signature); + } + + /// @notice Publish a signed decrypt result for an euint8 + function publishDecryptResult(euint8 input, uint8 result, bytes memory signature) internal { + Impl.publishDecryptResult(euint8.unwrap(input), uint256(result), signature); + } + + /// @notice Publish a signed decrypt result for an euint16 + function publishDecryptResult(euint16 input, uint16 result, bytes memory signature) internal { + Impl.publishDecryptResult(euint16.unwrap(input), uint256(result), signature); + } + + /// @notice Publish a signed decrypt result for an euint32 + function publishDecryptResult(euint32 input, uint32 result, bytes memory signature) internal { + Impl.publishDecryptResult(euint32.unwrap(input), uint256(result), signature); + } + + /// @notice Publish a signed decrypt result for an euint64 + function publishDecryptResult(euint64 input, uint64 result, bytes memory signature) internal { + Impl.publishDecryptResult(euint64.unwrap(input), uint256(result), signature); + } + + /// @notice Publish a signed decrypt result for an euint128 + function publishDecryptResult(euint128 input, uint128 result, bytes memory signature) internal { + Impl.publishDecryptResult(euint128.unwrap(input), uint256(result), signature); + } + + /// @notice Publish a signed decrypt result for an eaddress + function publishDecryptResult(eaddress input, address result, bytes memory signature) internal { + Impl.publishDecryptResult(eaddress.unwrap(input), uint256(uint160(result)), signature); + } + + /// @notice Publish multiple decrypt results in one transaction + /// @dev Amortizes base tx cost across multiple operations + function publishDecryptResultBatch(uint256[] memory ctHashes, uint256[] memory results, bytes[] memory signatures) internal { + bytes32[] memory ctHashesBytes32 = new bytes32[](ctHashes.length); + for (uint256 i = 0; i < ctHashes.length; i++) { + ctHashesBytes32[i] = bytes32(ctHashes[i]); + } + Impl.publishDecryptResultBatch(ctHashesBytes32, results, signatures); + } + + // ********** VERIFY DECRYPT RESULT ************* // + + /// @notice Verify a decrypt result signature without publishing + /// @param ctHash The ciphertext hash + /// @param result The decrypted plaintext value + /// @param signature The ECDSA signature from the decrypt network + /// @return True if signature is valid + function verifyDecryptResult(uint256 ctHash, uint256 result, bytes memory signature) internal view returns (bool) { + return Impl.verifyDecryptResult(bytes32(ctHash), result, signature); + } + + /// @notice Verify a decrypt result signature for an ebool + function verifyDecryptResult(ebool input, bool result, bytes memory signature) internal view returns (bool) { + return Impl.verifyDecryptResult(ebool.unwrap(input), result ? 1 : 0, signature); + } + + /// @notice Verify a decrypt result signature for an euint8 + function verifyDecryptResult(euint8 input, uint8 result, bytes memory signature) internal view returns (bool) { + return Impl.verifyDecryptResult(euint8.unwrap(input), uint256(result), signature); + } + + /// @notice Verify a decrypt result signature for an euint16 + function verifyDecryptResult(euint16 input, uint16 result, bytes memory signature) internal view returns (bool) { + return Impl.verifyDecryptResult(euint16.unwrap(input), uint256(result), signature); + } + + /// @notice Verify a decrypt result signature for an euint32 + function verifyDecryptResult(euint32 input, uint32 result, bytes memory signature) internal view returns (bool) { + return Impl.verifyDecryptResult(euint32.unwrap(input), uint256(result), signature); + } + + /// @notice Verify a decrypt result signature for an euint64 + function verifyDecryptResult(euint64 input, uint64 result, bytes memory signature) internal view returns (bool) { + return Impl.verifyDecryptResult(euint64.unwrap(input), uint256(result), signature); + } + + /// @notice Verify a decrypt result signature for an euint128 + function verifyDecryptResult(euint128 input, uint128 result, bytes memory signature) internal view returns (bool) { + return Impl.verifyDecryptResult(euint128.unwrap(input), uint256(result), signature); + } + + /// @notice Verify a decrypt result signature for an eaddress + function verifyDecryptResult(eaddress input, address result, bytes memory signature) internal view returns (bool) { + return Impl.verifyDecryptResult(eaddress.unwrap(input), uint256(uint160(result)), signature); + } + + // ********** VERIFY DECRYPT RESULT SAFE ************* // + + /// @notice Verify a decrypt result signature without publishing (non-reverting) + /// @param ctHash The ciphertext hash + /// @param result The decrypted plaintext value + /// @param signature The ECDSA signature from the decrypt network + /// @return True if signature is valid, false otherwise + function verifyDecryptResultSafe(uint256 ctHash, uint256 result, bytes memory signature) internal view returns (bool) { + return Impl.verifyDecryptResultSafe(bytes32(ctHash), result, signature); + } + + /// @notice Verify a decrypt result signature for an ebool (non-reverting) + function verifyDecryptResultSafe(ebool input, bool result, bytes memory signature) internal view returns (bool) { + return Impl.verifyDecryptResultSafe(ebool.unwrap(input), result ? 1 : 0, signature); + } + + /// @notice Verify a decrypt result signature for an euint8 (non-reverting) + function verifyDecryptResultSafe(euint8 input, uint8 result, bytes memory signature) internal view returns (bool) { + return Impl.verifyDecryptResultSafe(euint8.unwrap(input), uint256(result), signature); + } + + /// @notice Verify a decrypt result signature for an euint16 (non-reverting) + function verifyDecryptResultSafe(euint16 input, uint16 result, bytes memory signature) internal view returns (bool) { + return Impl.verifyDecryptResultSafe(euint16.unwrap(input), uint256(result), signature); + } + + /// @notice Verify a decrypt result signature for an euint32 (non-reverting) + function verifyDecryptResultSafe(euint32 input, uint32 result, bytes memory signature) internal view returns (bool) { + return Impl.verifyDecryptResultSafe(euint32.unwrap(input), uint256(result), signature); + } + + /// @notice Verify a decrypt result signature for an euint64 (non-reverting) + function verifyDecryptResultSafe(euint64 input, uint64 result, bytes memory signature) internal view returns (bool) { + return Impl.verifyDecryptResultSafe(euint64.unwrap(input), uint256(result), signature); + } + + /// @notice Verify a decrypt result signature for an euint128 (non-reverting) + function verifyDecryptResultSafe(euint128 input, uint128 result, bytes memory signature) internal view returns (bool) { + return Impl.verifyDecryptResultSafe(euint128.unwrap(input), uint256(result), signature); + } + + /// @notice Verify a decrypt result signature for an eaddress (non-reverting) + function verifyDecryptResultSafe(eaddress input, address result, bytes memory signature) internal view returns (bool) { + return Impl.verifyDecryptResultSafe(eaddress.unwrap(input), uint256(uint160(result)), signature); + } } // ********** BINDING DEFS ************* // @@ -3327,12 +3593,18 @@ library BindingsEbool { function isAllowed(ebool ctHash, address account) internal returns (bool) { return FHE.isAllowed(ctHash, account); } + function isPubliclyAllowed(ebool ctHash) internal view returns (bool) { + return FHE.isPubliclyAllowed(ctHash); + } function allowThis(ebool ctHash) internal { FHE.allowThis(ctHash); } function allowGlobal(ebool ctHash) internal { FHE.allowGlobal(ctHash); } + function allowPublic(ebool ctHash) internal { + FHE.allowPublic(ctHash); + } function allowSender(ebool ctHash) internal { FHE.allowSender(ctHash); } @@ -3563,12 +3835,18 @@ library BindingsEuint8 { function isAllowed(euint8 ctHash, address account) internal returns (bool) { return FHE.isAllowed(ctHash, account); } + function isPubliclyAllowed(euint8 ctHash) internal view returns (bool) { + return FHE.isPubliclyAllowed(ctHash); + } function allowThis(euint8 ctHash) internal { FHE.allowThis(ctHash); } function allowGlobal(euint8 ctHash) internal { FHE.allowGlobal(ctHash); } + function allowPublic(euint8 ctHash) internal { + FHE.allowPublic(ctHash); + } function allowSender(euint8 ctHash) internal { FHE.allowSender(ctHash); } @@ -3799,12 +4077,18 @@ library BindingsEuint16 { function isAllowed(euint16 ctHash, address account) internal returns (bool) { return FHE.isAllowed(ctHash, account); } + function isPubliclyAllowed(euint16 ctHash) internal view returns (bool) { + return FHE.isPubliclyAllowed(ctHash); + } function allowThis(euint16 ctHash) internal { FHE.allowThis(ctHash); } function allowGlobal(euint16 ctHash) internal { FHE.allowGlobal(ctHash); } + function allowPublic(euint16 ctHash) internal { + FHE.allowPublic(ctHash); + } function allowSender(euint16 ctHash) internal { FHE.allowSender(ctHash); } @@ -4035,12 +4319,18 @@ library BindingsEuint32 { function isAllowed(euint32 ctHash, address account) internal returns (bool) { return FHE.isAllowed(ctHash, account); } + function isPubliclyAllowed(euint32 ctHash) internal view returns (bool) { + return FHE.isPubliclyAllowed(ctHash); + } function allowThis(euint32 ctHash) internal { FHE.allowThis(ctHash); } function allowGlobal(euint32 ctHash) internal { FHE.allowGlobal(ctHash); } + function allowPublic(euint32 ctHash) internal { + FHE.allowPublic(ctHash); + } function allowSender(euint32 ctHash) internal { FHE.allowSender(ctHash); } @@ -4253,12 +4543,18 @@ library BindingsEuint64 { function isAllowed(euint64 ctHash, address account) internal returns (bool) { return FHE.isAllowed(ctHash, account); } + function isPubliclyAllowed(euint64 ctHash) internal view returns (bool) { + return FHE.isPubliclyAllowed(ctHash); + } function allowThis(euint64 ctHash) internal { FHE.allowThis(ctHash); } function allowGlobal(euint64 ctHash) internal { FHE.allowGlobal(ctHash); } + function allowPublic(euint64 ctHash) internal { + FHE.allowPublic(ctHash); + } function allowSender(euint64 ctHash) internal { FHE.allowSender(ctHash); } @@ -4454,12 +4750,18 @@ library BindingsEuint128 { function isAllowed(euint128 ctHash, address account) internal returns (bool) { return FHE.isAllowed(ctHash, account); } + function isPubliclyAllowed(euint128 ctHash) internal view returns (bool) { + return FHE.isPubliclyAllowed(ctHash); + } function allowThis(euint128 ctHash) internal { FHE.allowThis(ctHash); } function allowGlobal(euint128 ctHash) internal { FHE.allowGlobal(ctHash); } + function allowPublic(euint128 ctHash) internal { + FHE.allowPublic(ctHash); + } function allowSender(euint128 ctHash) internal { FHE.allowSender(ctHash); } @@ -4515,12 +4817,18 @@ library BindingsEaddress { function isAllowed(eaddress ctHash, address account) internal returns (bool) { return FHE.isAllowed(ctHash, account); } + function isPubliclyAllowed(eaddress ctHash) internal view returns (bool) { + return FHE.isPubliclyAllowed(ctHash); + } function allowThis(eaddress ctHash) internal { FHE.allowThis(ctHash); } function allowGlobal(eaddress ctHash) internal { FHE.allowGlobal(ctHash); } + function allowPublic(eaddress ctHash) internal { + FHE.allowPublic(ctHash); + } function allowSender(eaddress ctHash) internal { FHE.allowSender(ctHash); } diff --git a/contracts/ICofhe.sol b/contracts/ICofhe.sol index c05a8e3..67518ae 100644 --- a/contracts/ICofhe.sol +++ b/contracts/ICofhe.sol @@ -103,10 +103,20 @@ interface ITaskManager { function allow(uint256 ctHash, address account) external; function isAllowed(uint256 ctHash, address account) external returns (bool); + function isPubliclyAllowed(uint256 ctHash) external view returns (bool); function allowGlobal(uint256 ctHash) external; function allowTransient(uint256 ctHash, address account) external; function getDecryptResultSafe(uint256 ctHash) external view returns (uint256, bool); function getDecryptResult(uint256 ctHash) external view returns (uint256); + + function publishDecryptResult(uint256 ctHash, uint256 result, bytes calldata signature) external; + function publishDecryptResultBatch(uint256[] calldata ctHashes, uint256[] calldata results, bytes[] calldata signatures) external; + function verifyDecryptResult(uint256 ctHash, uint256 result, bytes calldata signature) external view returns (bool); + function verifyDecryptResultSafe( + uint256 ctHash, + uint256 result, + bytes calldata signature + ) external view returns (bool); } library Utils { diff --git a/contracts/internal/host-chain/.env.example b/contracts/internal/host-chain/.env.example index a9bed71..c790c3b 100644 --- a/contracts/internal/host-chain/.env.example +++ b/contracts/internal/host-chain/.env.example @@ -11,4 +11,6 @@ KEY="0xb03608c1c1f1461ed55c1c2315ab27fe82e2fe4c289ff753dea99078848d27dd" KEY2="0xcb5790da63720727af975f42c79f69918580209889225fa7128c92402a6d3a65" AGGREGATOR_KEY="dbfae500d71337029492a6f7f6c82e014467d1a847b684a9bca8403fbc0d6e45" # verifier signer address to be set only on deployment -VERIFIER_ADDRESS="0x0000000000000000000000000000000000000000" \ No newline at end of file +VERIFIER_ADDRESS="0x0000000000000000000000000000000000000000" +# decrypt result signer address (dispatcher's signing key address) +DECRYPT_RESULT_SIGNER="0x0000000000000000000000000000000000000000" \ No newline at end of file diff --git a/contracts/internal/host-chain/contracts/TaskManager.sol b/contracts/internal/host-chain/contracts/TaskManager.sol index 81e9867..42aa242 100644 --- a/contracts/internal/host-chain/contracts/TaskManager.sol +++ b/contracts/internal/host-chain/contracts/TaskManager.sol @@ -25,6 +25,7 @@ error InvalidSecurityZone(int32 zone, int32 min, int32 max); error InvalidSignature(); error InvalidSigner(address signer, address expectedSigner); error UnsupportedType(uint256 t); +error LengthMismatch(); // Access control errors error InvalidAddress(); @@ -48,6 +49,13 @@ library TMCommon { The format: keccak256(operands_list, op)[0:29] || is_trivial (1 bit) & ct_type (7 bit) || securityZone */ + // Constants for decrypt result hash computation (message format: result || enc_type || chain_id || ct_hash) + uint256 internal constant SHIFT_ENC_TYPE = 224; // Shift for 4-byte enc_type (256 - 32 = 224) + uint256 internal constant SHIFT_CHAIN_ID = 192; // Shift for 8-byte chain_id (256 - 64 = 192) + uint256 internal constant OFFSET_ENC_TYPE = 0x20; // Byte offset for enc_type in message + uint256 internal constant OFFSET_CHAIN_ID = 0x24; // Byte offset for chain_id in message + uint256 internal constant OFFSET_CT_HASH = 0x2c; // Byte offset for ctHash in message + uint256 internal constant MESSAGE_LENGTH = 0x4c; // Total message length: 76 bytes function uint256ToBytes32(uint256 value) internal pure returns (bytes memory) { bytes memory result = new bytes(32); @@ -161,6 +169,7 @@ contract TaskManager is ITaskManager, Initializable, UUPSUpgradeable, Ownable2St __UUPSUpgradeable_init(); initialized = true; verifierSigner = address(1); + decryptResultSigner = address(1); isEnabled = true; } @@ -193,6 +202,8 @@ contract TaskManager is ITaskManager, Initializable, UUPSUpgradeable, Ownable2St event TaskCreated(uint256 ctHash, string operation, uint256 input1, uint256 input2, uint256 input3); event ProtocolNotification(uint256 ctHash, string operation, string errorMessage); event DecryptionResult(uint256 ctHash, uint256 result, address indexed requestor); + event DecryptResultSignerChanged(address indexed oldSigner, address indexed newSigner); + event VerifierSignerChanged(address indexed oldSigner, address indexed newSigner); struct Task { address creator; @@ -225,6 +236,10 @@ contract TaskManager is ITaskManager, Initializable, UUPSUpgradeable, Ownable2St // If disabled, all operations will revert bool public isEnabled; + // Signer address for decrypt result verification (threshold network's signing key) + // When set to address(0), signature verification is skipped (debug mode) + address public decryptResultSigner; + modifier onlyAggregator() { if (!aggregators[msg.sender]) { @@ -549,6 +564,119 @@ contract TaskManager is ITaskManager, Initializable, UUPSUpgradeable, Ownable2St } } + /// @notice Publish a signed decrypt result to the chain + /// @dev Anyone with a valid signature from the decrypt network can call this + /// @param ctHash The ciphertext hash + /// @param result The decrypted plaintext value + /// @param signature The ECDSA signature from the decrypt network + function publishDecryptResult( + uint256 ctHash, + uint256 result, + bytes calldata signature + ) external onlyIfEnabled { + _verifyDecryptResult(ctHash, result, signature, true); + plaintextsStorage.storeResult(ctHash, result); + emit DecryptionResult(ctHash, result, msg.sender); + } + + /// @notice Publish multiple decrypt results in one transaction + /// @dev Amortizes base tx cost across multiple operations + function publishDecryptResultBatch( + uint256[] calldata ctHashes, + uint256[] calldata results, + bytes[] calldata signatures + ) external onlyIfEnabled { + uint256 length = ctHashes.length; + if (results.length != length || signatures.length != length) revert LengthMismatch(); + + for (uint256 i = 0; i < length; i++) { + _verifyDecryptResult(ctHashes[i], results[i], signatures[i], true); + plaintextsStorage.storeResult(ctHashes[i], results[i]); + emit DecryptionResult(ctHashes[i], results[i], msg.sender); + } + } + + /// @notice Verify a decrypt result signature without publishing + /// @dev Returns true if signature is valid, reverts otherwise + /// @return True if signature is valid + function verifyDecryptResult( + uint256 ctHash, + uint256 result, + bytes calldata signature + ) external view returns (bool) { + return _verifyDecryptResult(ctHash, result, signature, true); + } + + /// @notice Verify a decrypt result signature without publishing (non-reverting) + /// @dev Returns false if signature is invalid instead of reverting + /// @return True if signature is valid, false otherwise + function verifyDecryptResultSafe( + uint256 ctHash, + uint256 result, + bytes calldata signature + ) external view returns (bool) { + return _verifyDecryptResult(ctHash, result, signature, false); + } + + /// @dev Verify decrypt result signature + /// @dev Skips verification if decryptResultSigner is address(0) (debug mode) + /// @param shouldRevert If true, reverts on invalid signature; if false, returns false + function _verifyDecryptResult( + uint256 ctHash, + uint256 result, + bytes calldata signature, + bool shouldRevert + ) private view returns (bool) { + if (decryptResultSigner == address(0)) { + return true; + } + + bytes32 messageHash = _computeDecryptResultHash(ctHash, result); + (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(messageHash, signature); + + if (err != ECDSA.RecoverError.NoError || recovered == address(0)) { + if (shouldRevert) revert InvalidSignature(); + return false; + } + if (recovered != decryptResultSigner) { + if (shouldRevert) revert InvalidSigner(recovered, decryptResultSigner); + return false; + } + return true; + } + + /// @dev Compute message hash using assembly for gas efficiency + /// @notice Format: result (32) || enc_type (4) || chain_id (8) || ct_hash (32) = 76 bytes + function _computeDecryptResultHash( + uint256 ctHash, + uint256 result + ) private view returns (bytes32 messageHash) { + uint8 encryptionType = TMCommon.getUintTypeFromHash(ctHash); + uint64 chainId = uint64(block.chainid); + + // Load constants for assembly + uint256 shiftEncType = TMCommon.SHIFT_ENC_TYPE; + uint256 shiftChainId = TMCommon.SHIFT_CHAIN_ID; + uint256 offsetEncType = TMCommon.OFFSET_ENC_TYPE; + uint256 offsetChainId = TMCommon.OFFSET_CHAIN_ID; + uint256 offsetCtHash = TMCommon.OFFSET_CT_HASH; + uint256 msgLength = TMCommon.MESSAGE_LENGTH; + + // Assembly for gas-efficient message construction + // Overlapping 32-byte mstores are safe here: each subsequent mstore overwrites + // only the tail bytes of the previous one, and the final mstore (ctHash) lands + // exactly at the end of the 76-byte message, so all fields end up correctly placed. + assembly { + let ptr := mload(0x40) + mstore(ptr, result) // bytes 0-31: result + mstore(add(ptr, offsetEncType), shl(shiftEncType, encryptionType)) // bytes 32-35: enc_type + mstore(add(ptr, offsetChainId), shl(shiftChainId, chainId)) // bytes 36-43: chain_id + mstore(add(ptr, offsetCtHash), ctHash) // bytes 44-75: ctHash + messageHash := keccak256(ptr, msgLength) // hash 76 bytes + mstore(0x40, add(ptr, msgLength)) // advance free memory pointer + } + } + function handleError(uint256 ctHash, string memory operation, string memory errorMessage) external onlyAggregator { emit ProtocolNotification(ctHash, operation, errorMessage); } @@ -609,6 +737,10 @@ contract TaskManager is ITaskManager, Initializable, UUPSUpgradeable, Ownable2St return acl.isAllowed(ctHash, account); } + function isPubliclyAllowed(uint256 ctHash) external view returns (bool) { + return acl.globalAllowed(ctHash); + } + function extractSigner(EncryptedInput memory input, address sender) private view returns (address) { bytes memory combined = abi.encodePacked( input.ctHash, @@ -629,7 +761,17 @@ contract TaskManager is ITaskManager, Initializable, UUPSUpgradeable, Ownable2St } function setVerifierSigner(address signer) external onlyOwner { + address oldSigner = verifierSigner; verifierSigner = signer; + emit VerifierSignerChanged(oldSigner, signer); + } + + /// @notice Set the authorized signer for decrypt results + /// @param signer The new signer address (address(0) disables verification) + function setDecryptResultSigner(address signer) external onlyOwner { + address oldSigner = decryptResultSigner; + decryptResultSigner = signer; + emit DecryptResultSignerChanged(oldSigner, signer); } function setSecurityZoneMax(int32 securityZone) external onlyOwner { diff --git a/contracts/internal/host-chain/contracts/tests/OnChain.sol b/contracts/internal/host-chain/contracts/tests/OnChain.sol index b3ee63c..abac5f6 100644 --- a/contracts/internal/host-chain/contracts/tests/OnChain.sol +++ b/contracts/internal/host-chain/contracts/tests/OnChain.sol @@ -153,7 +153,7 @@ contract OnChain { } function cantEncryptWithFakeSecurityZone() public returns (euint32) { - return FHE.asEuint32(16, 100); + return FHE.asEuint32(16, 200); // 200 is outside valid range (-128 to 127) } function cantCastWithFakeType() public returns (uint256) { diff --git a/contracts/internal/host-chain/contracts/tests/PubliclyAllowedTest.sol b/contracts/internal/host-chain/contracts/tests/PubliclyAllowedTest.sol new file mode 100644 index 0000000..95d854b --- /dev/null +++ b/contracts/internal/host-chain/contracts/tests/PubliclyAllowedTest.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.13 <0.9.0; + +import {FHE, euint8} from "@fhenixprotocol/cofhe-contracts/FHE.sol"; + +contract PubliclyAllowedTest { + euint8 public lastHandle; + + function createAndAllowGlobal(uint8 value) public returns (euint8) { + euint8 encrypted = FHE.asEuint8(value); + FHE.allowGlobal(encrypted); + lastHandle = encrypted; + return encrypted; + } + + function createWithoutGlobal(uint8 value) public returns (euint8) { + euint8 encrypted = FHE.asEuint8(value); + lastHandle = encrypted; + return encrypted; + } +} diff --git a/contracts/internal/host-chain/deploy/deploy.ts b/contracts/internal/host-chain/deploy/deploy.ts index 51b59d1..39ec4d4 100644 --- a/contracts/internal/host-chain/deploy/deploy.ts +++ b/contracts/internal/host-chain/deploy/deploy.ts @@ -122,11 +122,39 @@ async function TaskManagerSetup(TMProxyContract: any, aggregatorSigners: any[]) process.env.VERIFIER_ADDRESS, ); await tx.wait(); - console.log(chalk.green("Successfully set verifier signer address")); + console.log(chalk.green(`Successfully set verifier signer address: ${process.env.VERIFIER_ADDRESS}`)); } catch (e) { console.error(chalk.red(`Failed setVerifierSigner transaction: ${e}`)); return e; } + + // Set the decrypt result signer (dispatcher's signing key) + try { + const connectedImplementation = TMProxyContract.connect(aggregatorSigners[0]); + if (process.env.DECRYPT_RESULT_SIGNER === "0x0000000000000000000000000000000000000000") { + const networkName = hre?.network?.name; + const networkConfig = hre?.network?.config as any; + const networkUrl = networkConfig?.url; + if ( + networkUrl && + !networkUrl.includes("localhost") && + !networkUrl.includes("127.0.0.1") && + !networkName?.startsWith("localfhenix") + ) { + console.error(chalk.red("refusing to set DECRYPT_RESULT_SIGNER to 0 on a non-local network!")); + return; + } + } + + const tx = await connectedImplementation.setDecryptResultSigner( + process.env.DECRYPT_RESULT_SIGNER, + ); + await tx.wait(); + console.log(chalk.green(`Successfully set decrypt result signer address: ${process.env.DECRYPT_RESULT_SIGNER}`)); + } catch (e) { + console.error(chalk.red(`Failed setDecryptResultSigner transaction: ${e}`)); + return e; + } console.log("\n"); } @@ -301,7 +329,7 @@ function getAggregatorWallets(ethers: any) { const func: DeployFunction = async function () { console.log(chalk.bold.blue("-----------------------Network-----------------------------")); console.log(chalk.green("Network name:", hre.network.name)); - console.log(chalk.green("Network:", hre.network.config)); + console.log(chalk.green("Network:", JSON.stringify(hre.network.config, (_, v) => typeof v === 'bigint' ? v.toString() : v))); console.log("\n"); // Note: we need to use an unused account for deployment via ignition, or it will complain diff --git a/contracts/internal/host-chain/package.json b/contracts/internal/host-chain/package.json index 7f2cdb2..26f0fb5 100644 --- a/contracts/internal/host-chain/package.json +++ b/contracts/internal/host-chain/package.json @@ -3,7 +3,11 @@ "version": "1.0.0", "description": "Internal contracts for the CoFHE", "scripts": { - "test": "hardhat test", + "test": "hardhat test --network hardhat", + "test:decrypt-result": "hardhat test test/decryptResult/DecryptResult.ts --network hardhat", + "test:localfhenix": "hardhat test", + "test:decrypt-result:localfhenix": "hardhat test test/decryptResult/DecryptResult.ts", + "test:onchain": "hardhat test test/onChain/OnChain.ts", "clean": "rimraf ./artifacts ./cache ./coverage ./types ./coverage.json ./deployments/localfhenix ./ignition/deployments ./.openzeppelin/*", "compile": "hardhat compile", "task:deploy": "hardhat deploy", diff --git a/contracts/internal/host-chain/test/decryptResult/DecryptResult.behavior.ts b/contracts/internal/host-chain/test/decryptResult/DecryptResult.behavior.ts new file mode 100644 index 0000000..776771e --- /dev/null +++ b/contracts/internal/host-chain/test/decryptResult/DecryptResult.behavior.ts @@ -0,0 +1,733 @@ +import { expect } from "chai"; +import hre from "hardhat"; +const { ethers } = hre; +import { Wallet, keccak256, toUtf8Bytes, getBytes, zeroPadValue, toBeHex } from "ethers"; + +// Encryption type constants (must match Utils library in ICofhe.sol) +const EUINT8_TFHE = 2; +const EUINT16_TFHE = 3; +const EUINT32_TFHE = 4; +const EUINT64_TFHE = 5; +const EUINT128_TFHE = 6; +const EADDRESS_TFHE = 7; +const EBOOL_TFHE = 0; + +/** + * Build a ctHash with embedded type metadata + * Format: keccak256(data)[0:30] || type (1 byte) || security_zone (1 byte) + */ +function buildCtHash(baseHash: string, encryptionType: number, securityZone: number = 0): bigint { + const hash = BigInt(baseHash); + // Clear the last 2 bytes (16 bits) + const maskedHash = hash & (~BigInt(0xFFFF)); + // Embed type in bits 8-14 (7 bits for type, 1 bit for trivial flag) + const typeShifted = BigInt(encryptionType) << BigInt(8); + // Security zone in last byte + const szByte = BigInt(securityZone & 0xFF); + return maskedHash | typeShifted | szByte; +} + +/** + * Compute the message hash that matches Solidity's _computeDecryptResultHash assembly + * Format: result (32) || enc_type (4) || chain_id (8) || ct_hash (32) = 76 bytes + */ +function computeDecryptResultHash( + result: bigint, + encryptionType: number, + chainId: bigint, + ctHash: bigint +): string { + // Build 76-byte buffer exactly matching Solidity assembly + const buffer = new Uint8Array(76); + + // result: 32 bytes (big-endian) + const resultBytes = getBytes(zeroPadValue(toBeHex(result), 32)); + buffer.set(resultBytes, 0); + + // encryption_type: 4 bytes (i32, big-endian) + const encTypeBytes = new Uint8Array(4); + encTypeBytes[0] = (encryptionType >> 24) & 0xFF; + encTypeBytes[1] = (encryptionType >> 16) & 0xFF; + encTypeBytes[2] = (encryptionType >> 8) & 0xFF; + encTypeBytes[3] = encryptionType & 0xFF; + buffer.set(encTypeBytes, 32); + + // chain_id: 8 bytes (u64, big-endian) + const chainIdBytes = new Uint8Array(8); + const chainIdBigInt = BigInt(chainId); + for (let i = 7; i >= 0; i--) { + chainIdBytes[7 - i] = Number((chainIdBigInt >> BigInt(i * 8)) & BigInt(0xFF)); + } + buffer.set(chainIdBytes, 36); + + // ct_hash: 32 bytes (big-endian) + const ctHashBytes = getBytes(zeroPadValue(toBeHex(ctHash), 32)); + buffer.set(ctHashBytes, 44); + + return keccak256(buffer); +} + +/** + * Sign a decrypt result using the same format as the TN dispatcher + */ +async function signDecryptResult( + signer: Wallet, + result: bigint, + encryptionType: number, + chainId: bigint, + ctHash: bigint +): Promise { + // Compute message hash matching Solidity's assembly + const messageHash = computeDecryptResultHash(result, encryptionType, chainId, ctHash); + + // Sign the hash directly (not with personal_sign prefix - matches TN's sign_prehash) + const signature = signer.signingKey.sign(messageHash); + + // Return 65-byte signature as hex (r + s + v) + return signature.r.slice(2) + signature.s.slice(2) + signature.v.toString(16).padStart(2, "0"); +} + +export function shouldBehaveLikeDecryptResult(): void { + describe("publishDecryptResult", function () { + it("should store result with valid signature", async function () { + const taskManager = this.taskManager as Contract; + const testSigner = this.testSigner as Wallet; + + const chainId = BigInt((await ethers.provider.getNetwork()).chainId); + const baseHash = keccak256(toUtf8Bytes("test-cthash-1")); + const ctHash = buildCtHash(baseHash, EUINT64_TFHE); + const result = BigInt(42); + + const signature = await signDecryptResult( + testSigner, + result, + EUINT64_TFHE, + chainId, + ctHash + ); + + // Publish the result + const tx = await taskManager.publishDecryptResult( + ctHash, + result, + "0x" + signature + ); + await tx.wait(); + + // Verify result was stored + const [storedResult, exists] = await taskManager.getDecryptResultSafe(ctHash); + expect(exists).to.be.true; + expect(storedResult).to.equal(result); + }); + + it("should emit DecryptionResult event", async function () { + const taskManager = this.taskManager as Contract; + const testSigner = this.testSigner as Wallet; + + const chainId = BigInt((await ethers.provider.getNetwork()).chainId); + const baseHash = keccak256(toUtf8Bytes("test-cthash-event")); + const ctHash = buildCtHash(baseHash, EUINT32_TFHE); + const result = BigInt(123); + + const signature = await signDecryptResult( + testSigner, + result, + EUINT32_TFHE, + chainId, + ctHash + ); + + await expect( + taskManager.publishDecryptResult(ctHash, result, "0x" + signature) + ).to.emit(taskManager, "DecryptionResult"); + }); + + it("should revert with invalid signature", async function () { + const taskManager = this.taskManager as Contract; + + const baseHash = keccak256(toUtf8Bytes("test-cthash-invalid")); + const ctHash = buildCtHash(baseHash, EUINT64_TFHE); + const result = BigInt(99); + + // Create a fake signature (65 bytes of zeros won't work) + const fakeSignature = "0x" + "00".repeat(65); + + // OpenZeppelin's ECDSA.recover throws ECDSAInvalidSignature for malformed signatures + await expect( + taskManager.publishDecryptResult(ctHash, result, fakeSignature) + ).to.be.reverted; + }); + + it("should revert with wrong signer", async function () { + const taskManager = this.taskManager as Contract; + + const chainId = BigInt((await ethers.provider.getNetwork()).chainId); + const baseHash = keccak256(toUtf8Bytes("test-cthash-wrong-signer")); + const ctHash = buildCtHash(baseHash, EUINT64_TFHE); + const result = BigInt(55); + + // Sign with a different key + const wrongSigner = new ethers.Wallet( + "0xabcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", + ethers.provider + ); + + const signature = await signDecryptResult( + wrongSigner, + result, + EUINT64_TFHE, + chainId, + ctHash + ); + + await expect( + taskManager.publishDecryptResult(ctHash, result, "0x" + signature) + ).to.be.revertedWithCustomError(taskManager, "InvalidSigner"); + }); + + it("should revert with tampered result", async function () { + const taskManager = this.taskManager as Contract; + const testSigner = this.testSigner as Wallet; + + const chainId = BigInt((await ethers.provider.getNetwork()).chainId); + const baseHash = keccak256(toUtf8Bytes("test-cthash-tampered")); + const ctHash = buildCtHash(baseHash, EUINT64_TFHE); + const originalResult = BigInt(100); + const tamperedResult = BigInt(999); // Different from signed value + + // Sign with original result + const signature = await signDecryptResult( + testSigner, + originalResult, + EUINT64_TFHE, + chainId, + ctHash + ); + + // Try to publish with tampered result + await expect( + taskManager.publishDecryptResult(ctHash, tamperedResult, "0x" + signature) + ).to.be.revertedWithCustomError(taskManager, "InvalidSigner"); + }); + + it("should revert when contract is disabled", async function () { + const taskManager = this.taskManager as Contract; + const owner = this.owner; + + // Disable the contract + await taskManager.connect(owner).disable(); + + const baseHash = keccak256(toUtf8Bytes("test-cthash-disabled")); + const ctHash = buildCtHash(baseHash, EUINT64_TFHE); + const result = BigInt(42); + + await expect( + taskManager.publishDecryptResult(ctHash, result, "0x" + "00".repeat(65)) + ).to.be.revertedWithCustomError(taskManager, "CofheIsUnavailable"); + + // Re-enable for other tests + await taskManager.connect(owner).enable(); + }); + + it("should work with different encryption types", async function () { + const taskManager = this.taskManager as Contract; + const testSigner = this.testSigner as Wallet; + + const chainId = BigInt((await ethers.provider.getNetwork()).chainId); + const encryptionTypes = [EBOOL_TFHE, EUINT8_TFHE, EUINT16_TFHE, EUINT32_TFHE, EUINT64_TFHE, EUINT128_TFHE, EADDRESS_TFHE]; + + for (const encType of encryptionTypes) { + const baseHash = keccak256(toUtf8Bytes(`test-cthash-type-${encType}`)); + const ctHash = buildCtHash(baseHash, encType); + const result = BigInt(encType + 10); + + const signature = await signDecryptResult( + testSigner, + result, + encType, + chainId, + ctHash + ); + + const tx = await taskManager.publishDecryptResult( + ctHash, + result, + "0x" + signature + ); + await tx.wait(); + + const [storedResult, exists] = await taskManager.getDecryptResultSafe(ctHash); + expect(exists).to.be.true; + expect(storedResult).to.equal(result); + } + }); + }); + + describe("publishDecryptResultBatch", function () { + it("should store multiple results in one transaction", async function () { + const taskManager = this.taskManager as Contract; + const testSigner = this.testSigner as Wallet; + + const chainId = BigInt((await ethers.provider.getNetwork()).chainId); + const count = 3; + const ctHashes: bigint[] = []; + const results: bigint[] = []; + const signatures: string[] = []; + + for (let i = 0; i < count; i++) { + const baseHash = keccak256(toUtf8Bytes(`batch-cthash-${i}`)); + const ctHash = buildCtHash(baseHash, EUINT64_TFHE); + const result = BigInt(i * 100 + 1); + + const signature = await signDecryptResult( + testSigner, + result, + EUINT64_TFHE, + chainId, + ctHash + ); + + ctHashes.push(ctHash); + results.push(result); + signatures.push("0x" + signature); + } + + const tx = await taskManager.publishDecryptResultBatch( + ctHashes, + results, + signatures + ); + await tx.wait(); + + // Verify all results were stored + for (let i = 0; i < count; i++) { + const [storedResult, exists] = await taskManager.getDecryptResultSafe(ctHashes[i]); + expect(exists).to.be.true; + expect(storedResult).to.equal(results[i]); + } + }); + + it("should revert entire batch if one signature is invalid", async function () { + const taskManager = this.taskManager as Contract; + const testSigner = this.testSigner as Wallet; + + const chainId = BigInt((await ethers.provider.getNetwork()).chainId); + const ctHashes: bigint[] = []; + const results: bigint[] = []; + const signatures: string[] = []; + + // First valid entry + const baseHash1 = keccak256(toUtf8Bytes("batch-fail-1")); + const ctHash1 = buildCtHash(baseHash1, EUINT64_TFHE); + const result1 = BigInt(111); + const sig1 = await signDecryptResult(testSigner, result1, EUINT64_TFHE, chainId, ctHash1); + + ctHashes.push(ctHash1); + results.push(result1); + signatures.push("0x" + sig1); + + // Second invalid entry (bad signature) + const baseHash2 = keccak256(toUtf8Bytes("batch-fail-2")); + const ctHash2 = buildCtHash(baseHash2, EUINT64_TFHE); + const result2 = BigInt(222); + + ctHashes.push(ctHash2); + results.push(result2); + signatures.push("0x" + "00".repeat(65)); // Invalid signature + + // OpenZeppelin's ECDSA.recover throws ECDSAInvalidSignature for malformed signatures + await expect( + taskManager.publishDecryptResultBatch(ctHashes, results, signatures) + ).to.be.reverted; + + // First entry should NOT have been stored (atomic) + const [, exists1] = await taskManager.getDecryptResultSafe(ctHash1); + expect(exists1).to.be.false; + }); + + it("should succeed with empty arrays", async function () { + const taskManager = this.taskManager as Contract; + + // Empty batch should succeed (no-op) + const tx = await taskManager.publishDecryptResultBatch([], [], []); + await tx.wait(); + }); + + it("should revert on length mismatch", async function () { + const taskManager = this.taskManager as Contract; + + await expect( + taskManager.publishDecryptResultBatch( + [BigInt(1), BigInt(2)], + [BigInt(10)], // Length mismatch + ["0x" + "00".repeat(65)] + ) + ).to.be.revertedWithCustomError(taskManager, "LengthMismatch"); + }); + }); + + describe("verifyDecryptResult", function () { + it("should return true for valid signature", async function () { + const taskManager = this.taskManager as Contract; + const testSigner = this.testSigner as Wallet; + + const chainId = BigInt((await ethers.provider.getNetwork()).chainId); + const baseHash = keccak256(toUtf8Bytes("verify-cthash-valid")); + const ctHash = buildCtHash(baseHash, EUINT64_TFHE); + const result = BigInt(777); + + const signature = await signDecryptResult( + testSigner, + result, + EUINT64_TFHE, + chainId, + ctHash + ); + + const isValid = await taskManager.verifyDecryptResult( + ctHash, + result, + "0x" + signature + ); + expect(isValid).to.be.true; + }); + + it("should not modify state (view function)", async function () { + const taskManager = this.taskManager as Contract; + const testSigner = this.testSigner as Wallet; + + const chainId = BigInt((await ethers.provider.getNetwork()).chainId); + const baseHash = keccak256(toUtf8Bytes("verify-no-state")); + const ctHash = buildCtHash(baseHash, EUINT64_TFHE); + const result = BigInt(888); + + const signature = await signDecryptResult( + testSigner, + result, + EUINT64_TFHE, + chainId, + ctHash + ); + + // Call verify + await taskManager.verifyDecryptResult(ctHash, result, "0x" + signature); + + // Result should NOT be stored + const [, exists] = await taskManager.getDecryptResultSafe(ctHash); + expect(exists).to.be.false; + }); + + it("should revert for invalid signature", async function () { + const taskManager = this.taskManager as Contract; + + const baseHash = keccak256(toUtf8Bytes("verify-invalid")); + const ctHash = buildCtHash(baseHash, EUINT64_TFHE); + const result = BigInt(999); + + // OpenZeppelin's ECDSA.recover throws ECDSAInvalidSignature for malformed signatures + await expect( + taskManager.verifyDecryptResult(ctHash, result, "0x" + "00".repeat(65)) + ).to.be.reverted; + }); + }); + + describe("Debug mode (signer = address(0))", function () { + it("should skip verification when decryptResultSigner is address(0)", async function () { + const taskManager = this.taskManager as Contract; + const owner = this.owner; + + // Set signer to address(0) to enable debug mode + await taskManager.connect(owner).setDecryptResultSigner(ethers.ZeroAddress); + + const baseHash = keccak256(toUtf8Bytes("debug-mode-test")); + const ctHash = buildCtHash(baseHash, EUINT64_TFHE); + const result = BigInt(12345); + + // Should succeed with any signature (even invalid) + const tx = await taskManager.publishDecryptResult( + ctHash, + result, + "0x" + "00".repeat(65) + ); + await tx.wait(); + + const [storedResult, exists] = await taskManager.getDecryptResultSafe(ctHash); + expect(exists).to.be.true; + expect(storedResult).to.equal(result); + + // Restore signer for other tests + await taskManager.connect(owner).setDecryptResultSigner(this.testSigner.address); + }); + + it("verifyDecryptResult should return true in debug mode", async function () { + const taskManager = this.taskManager as Contract; + const owner = this.owner; + + await taskManager.connect(owner).setDecryptResultSigner(ethers.ZeroAddress); + + const baseHash = keccak256(toUtf8Bytes("debug-verify")); + const ctHash = buildCtHash(baseHash, EUINT64_TFHE); + const result = BigInt(54321); + + const isValid = await taskManager.verifyDecryptResult( + ctHash, + result, + "0x" + "00".repeat(65) + ); + expect(isValid).to.be.true; + + // Restore signer + await taskManager.connect(owner).setDecryptResultSigner(this.testSigner.address); + }); + }); + + describe("setDecryptResultSigner", function () { + it("should emit DecryptResultSignerChanged event", async function () { + const taskManager = this.taskManager as Contract; + const owner = this.owner; + const testSigner = this.testSigner as Wallet; + + const newSigner = ethers.Wallet.createRandom().address; + + await expect(taskManager.connect(owner).setDecryptResultSigner(newSigner)) + .to.emit(taskManager, "DecryptResultSignerChanged") + .withArgs(testSigner.address, newSigner); + + // Restore original signer + await taskManager.connect(owner).setDecryptResultSigner(testSigner.address); + }); + + it("should only be callable by owner", async function () { + const taskManager = this.taskManager as Contract; + const otherAccount = this.otherAccount; + + const newSigner = ethers.Wallet.createRandom().address; + + await expect( + taskManager.connect(otherAccount).setDecryptResultSigner(newSigner) + ).to.be.revertedWithCustomError(taskManager, "OwnableUnauthorizedAccount"); + }); + }); + + describe("setVerifierSigner", function () { + it("should emit VerifierSignerChanged event", async function () { + const taskManager = this.taskManager as Contract; + const owner = this.owner; + + const originalSigner = await taskManager.verifierSigner(); + const newSigner = ethers.Wallet.createRandom().address; + + await expect(taskManager.connect(owner).setVerifierSigner(newSigner)) + .to.emit(taskManager, "VerifierSignerChanged") + .withArgs(originalSigner, newSigner); + + // Restore original signer + await taskManager.connect(owner).setVerifierSigner(originalSigner); + }); + + it("should only be callable by owner", async function () { + const taskManager = this.taskManager as Contract; + const otherAccount = this.otherAccount; + + const newSigner = ethers.Wallet.createRandom().address; + + await expect( + taskManager.connect(otherAccount).setVerifierSigner(newSigner) + ).to.be.revertedWithCustomError(taskManager, "OwnableUnauthorizedAccount"); + }); + }); + + describe("verifyDecryptResultSafe", function () { + it("should return true for valid signature", async function () { + const taskManager = this.taskManager as Contract; + const testSigner = this.testSigner as Wallet; + + const chainId = BigInt((await ethers.provider.getNetwork()).chainId); + const baseHash = keccak256(toUtf8Bytes("verify-safe-valid")); + const ctHash = buildCtHash(baseHash, EUINT64_TFHE); + const result = BigInt(777); + + const signature = await signDecryptResult( + testSigner, + result, + EUINT64_TFHE, + chainId, + ctHash + ); + + const isValid = await taskManager.verifyDecryptResultSafe( + ctHash, + result, + "0x" + signature + ); + expect(isValid).to.be.true; + }); + + it("should return false for invalid signature (not revert)", async function () { + const taskManager = this.taskManager as Contract; + const testSigner = this.testSigner as Wallet; + + const chainId = BigInt((await ethers.provider.getNetwork()).chainId); + const baseHash = keccak256(toUtf8Bytes("verify-safe-invalid")); + const ctHash = buildCtHash(baseHash, EUINT64_TFHE); + const result = BigInt(999); + + // Sign with correct signer but wrong result (simulates tampered data) + const signature = await signDecryptResult( + testSigner, + BigInt(123), // Different result than what we'll verify + EUINT64_TFHE, + chainId, + ctHash + ); + + // Should return false, NOT revert + const isValid = await taskManager.verifyDecryptResultSafe( + ctHash, + result, + "0x" + signature + ); + expect(isValid).to.be.false; + }); + + it("should return false for wrong signer (not revert)", async function () { + const taskManager = this.taskManager as Contract; + + const chainId = BigInt((await ethers.provider.getNetwork()).chainId); + const baseHash = keccak256(toUtf8Bytes("verify-safe-wrong-signer")); + const ctHash = buildCtHash(baseHash, EUINT64_TFHE); + const result = BigInt(555); + + // Sign with a different key + const wrongSigner = new ethers.Wallet( + "0xabcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", + ethers.provider + ); + + const signature = await signDecryptResult( + wrongSigner, + result, + EUINT64_TFHE, + chainId, + ctHash + ); + + // Should return false, NOT revert + const isValid = await taskManager.verifyDecryptResultSafe( + ctHash, + result, + "0x" + signature + ); + expect(isValid).to.be.false; + }); + + it("should return true in debug mode (signer = address(0))", async function () { + const taskManager = this.taskManager as Contract; + const owner = this.owner; + const testSigner = this.testSigner as Wallet; + + // Set signer to address(0) to enable debug mode + await taskManager.connect(owner).setDecryptResultSigner(ethers.ZeroAddress); + + const baseHash = keccak256(toUtf8Bytes("verify-safe-debug")); + const ctHash = buildCtHash(baseHash, EUINT64_TFHE); + const result = BigInt(12345); + + // Should return true with any signature in debug mode + const isValid = await taskManager.verifyDecryptResultSafe( + ctHash, + result, + "0x" + "00".repeat(65) + ); + expect(isValid).to.be.true; + + // Restore signer + await taskManager.connect(owner).setDecryptResultSigner(testSigner.address); + }); + + it("should not modify state (view function)", async function () { + const taskManager = this.taskManager as Contract; + const testSigner = this.testSigner as Wallet; + + const chainId = BigInt((await ethers.provider.getNetwork()).chainId); + const baseHash = keccak256(toUtf8Bytes("verify-safe-no-state")); + const ctHash = buildCtHash(baseHash, EUINT64_TFHE); + const result = BigInt(888); + + const signature = await signDecryptResult( + testSigner, + result, + EUINT64_TFHE, + chainId, + ctHash + ); + + // Call verifySafe + await taskManager.verifyDecryptResultSafe(ctHash, result, "0x" + signature); + + // Result should NOT be stored + const [, exists] = await taskManager.getDecryptResultSafe(ctHash); + expect(exists).to.be.false; + }); + + it("should return false for malformed signature (not revert)", async function () { + const taskManager = this.taskManager as Contract; + + const baseHash = keccak256(toUtf8Bytes("verify-safe-malformed")); + const ctHash = buildCtHash(baseHash, EUINT64_TFHE); + const result = BigInt(42); + + // Pass garbage bytes (wrong length) — should return false, not revert + const isValid = await taskManager.verifyDecryptResultSafe( + ctHash, + result, + "0xdead" + ); + expect(isValid).to.be.false; + }); + + it("should return false for empty signature (not revert)", async function () { + const taskManager = this.taskManager as Contract; + + const baseHash = keccak256(toUtf8Bytes("verify-safe-empty")); + const ctHash = buildCtHash(baseHash, EUINT64_TFHE); + const result = BigInt(42); + + // Pass empty bytes — should return false, not revert + const isValid = await taskManager.verifyDecryptResultSafe( + ctHash, + result, + "0x" + ); + expect(isValid).to.be.false; + }); + }); + + describe("Cross-chain replay protection", function () { + it("signature for one chain should not work on another", async function () { + const taskManager = this.taskManager as Contract; + const testSigner = this.testSigner as Wallet; + + const actualChainId = BigInt((await ethers.provider.getNetwork()).chainId); + const fakeChainId = actualChainId + BigInt(1); + + const baseHash = keccak256(toUtf8Bytes("replay-test")); + const ctHash = buildCtHash(baseHash, EUINT64_TFHE); + const result = BigInt(555); + + // Sign for a different chain + const signature = await signDecryptResult( + testSigner, + result, + EUINT64_TFHE, + fakeChainId, // Wrong chain + ctHash + ); + + // Should fail because chainId in signature doesn't match block.chainid + await expect( + taskManager.publishDecryptResult(ctHash, result, "0x" + signature) + ).to.be.revertedWithCustomError(taskManager, "InvalidSigner"); + }); + }); +} diff --git a/contracts/internal/host-chain/test/decryptResult/DecryptResult.fixture.ts b/contracts/internal/host-chain/test/decryptResult/DecryptResult.fixture.ts new file mode 100644 index 0000000..b222ec6 --- /dev/null +++ b/contracts/internal/host-chain/test/decryptResult/DecryptResult.fixture.ts @@ -0,0 +1,137 @@ +import hre from "hardhat"; +const { ethers } = hre; +import { Wallet, BaseContract } from "ethers"; + +// The hardcoded TaskManager address that ACL and PlaintextsStorage expect +const TASK_MANAGER_ADDRESS = "0xeA30c4B8b44078Bbf8a6ef5b9f1eC1626C7848D9"; + +export interface DecryptResultFixture { + taskManager: BaseContract; + plaintextsStorage: BaseContract; + acl: BaseContract; + owner: any; + testSigner: Wallet; + otherAccount: any; +} + +/** + * Generate a deterministic test signing key for testing + * This key is ONLY for testing - never use in production + */ +function getTestSignerWallet(): Wallet { + const testPrivateKey = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + return new ethers.Wallet(testPrivateKey, ethers.provider); +} + +/** + * Deploy a proxy at a specific address using hardhat_setCode + */ +async function deployProxyAtAddress( + targetAddress: string, + implementationAddress: string, + initData: string +): Promise { + // Get the proxy bytecode by deploying one temporarily + const ERC1967Proxy = await ethers.getContractFactory("ERC1967Proxy"); + const tempProxy = await ERC1967Proxy.deploy(implementationAddress, initData); + await tempProxy.waitForDeployment(); + + // Get the runtime bytecode from the deployed proxy + const proxyBytecode = await ethers.provider.getCode(await tempProxy.getAddress()); + + // Set the bytecode at our target address + await ethers.provider.send("hardhat_setCode", [targetAddress, proxyBytecode]); + + // Storage slots to copy (ERC1967 + OZ v5 namespaced storage) + const storageSlots = [ + // ERC1967 implementation slot + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + // OZ Initializable slot: keccak256("openzeppelin.storage.Initializable") - 1 + "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00", + // OZ Ownable slot: keccak256("openzeppelin.storage.Ownable") - 1 + "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300", + // OZ Ownable2Step pending owner slot (next slot after owner) + "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199301", + // TaskManager storage slots (slot 0-10 for custom state variables) + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000000000000000000000000000003", + "0x0000000000000000000000000000000000000000000000000000000000000004", + "0x0000000000000000000000000000000000000000000000000000000000000005", + "0x0000000000000000000000000000000000000000000000000000000000000006", + "0x0000000000000000000000000000000000000000000000000000000000000007", + "0x0000000000000000000000000000000000000000000000000000000000000008", + "0x0000000000000000000000000000000000000000000000000000000000000009", + "0x000000000000000000000000000000000000000000000000000000000000000a", + ]; + + const tempAddress = await tempProxy.getAddress(); + for (const slot of storageSlots) { + const value = await ethers.provider.getStorage(tempAddress, slot); + if (value !== "0x0000000000000000000000000000000000000000000000000000000000000000") { + await ethers.provider.send("hardhat_setStorageAt", [targetAddress, slot, value]); + } + } +} + +export async function deployDecryptResultFixture(): Promise { + const [owner, otherAccount] = await ethers.getSigners(); + + // Deploy TaskManager implementation + const TaskManager = await ethers.getContractFactory("TaskManager"); + const taskManagerImpl = await TaskManager.deploy(); + await taskManagerImpl.waitForDeployment(); + + // Prepare init data + const initData = TaskManager.interface.encodeFunctionData("initialize", [owner.address]); + + // Deploy proxy at the hardcoded address + await deployProxyAtAddress( + TASK_MANAGER_ADDRESS, + await taskManagerImpl.getAddress(), + initData + ); + + // Get TaskManager at the hardcoded address + const taskManager = TaskManager.attach(TASK_MANAGER_ADDRESS); + + // Deploy ACL (real contract - it expects TaskManager at hardcoded address) + const ACL = await ethers.getContractFactory("ACL"); + const aclImpl = await ACL.deploy(); + await aclImpl.waitForDeployment(); + + const ERC1967Proxy = await ethers.getContractFactory("ERC1967Proxy"); + const aclInitData = ACL.interface.encodeFunctionData("initialize", [owner.address]); + const aclProxy = await ERC1967Proxy.deploy(await aclImpl.getAddress(), aclInitData); + await aclProxy.waitForDeployment(); + const acl = ACL.attach(await aclProxy.getAddress()); + + // Deploy PlaintextsStorage (real contract) + const PlaintextsStorage = await ethers.getContractFactory("PlaintextsStorage"); + const psImpl = await PlaintextsStorage.deploy(); + await psImpl.waitForDeployment(); + + const psInitData = PlaintextsStorage.interface.encodeFunctionData("initialize", [owner.address]); + const psProxy = await ERC1967Proxy.deploy(await psImpl.getAddress(), psInitData); + await psProxy.waitForDeployment(); + const plaintextsStorage = PlaintextsStorage.attach(await psProxy.getAddress()); + + // Configure TaskManager + await taskManager.setACLContract(await acl.getAddress()); + await taskManager.setPlaintextsStorage(await plaintextsStorage.getAddress()); + await taskManager.setSecurityZones(-128, 127); + + // Create test signer + const testSigner = getTestSignerWallet(); + await taskManager.setDecryptResultSigner(testSigner.address); + + return { + taskManager, + plaintextsStorage, + acl, + owner, + testSigner, + otherAccount, + }; +} diff --git a/contracts/internal/host-chain/test/decryptResult/DecryptResult.ts b/contracts/internal/host-chain/test/decryptResult/DecryptResult.ts new file mode 100644 index 0000000..9783ca3 --- /dev/null +++ b/contracts/internal/host-chain/test/decryptResult/DecryptResult.ts @@ -0,0 +1,26 @@ +import type { Signers } from "../types"; +import { shouldBehaveLikeDecryptResult } from "./DecryptResult.behavior"; +import { deployDecryptResultFixture } from "./DecryptResult.fixture"; +import hre from "hardhat"; + +describe("DecryptResult Tests", function () { + before(async function () { + this.signers = {} as Signers; + + const fixture = await deployDecryptResultFixture(); + this.taskManager = fixture.taskManager; + this.plaintextsStorage = fixture.plaintextsStorage; + this.acl = fixture.acl; + this.owner = fixture.owner; + this.testSigner = fixture.testSigner; + this.otherAccount = fixture.otherAccount; + + // set admin account/signer + const signers = await hre.ethers.getSigners(); + this.signers.admin = signers[0]; + }); + + describe("PublishDecryptResult", function () { + shouldBehaveLikeDecryptResult(); + }); +}); diff --git a/contracts/internal/host-chain/test/onChain/OnChain.behavior.ts b/contracts/internal/host-chain/test/onChain/OnChain.behavior.ts index f0fc8fc..f31c5f4 100644 --- a/contracts/internal/host-chain/test/onChain/OnChain.behavior.ts +++ b/contracts/internal/host-chain/test/onChain/OnChain.behavior.ts @@ -17,7 +17,7 @@ export function shouldBehaveLikeOnChain(): void { console.log(`verified revert for type ${type}`); } - const types2 = ["64", "128", "256"]; + const types2 = ["64", "128"]; for (const type of types2) { const funcName = `trivial${type}`; console.log("funcName", funcName); diff --git a/contracts/internal/host-chain/test/onChain/OnChain.fixture.ts b/contracts/internal/host-chain/test/onChain/OnChain.fixture.ts index d25a62d..4e04046 100644 --- a/contracts/internal/host-chain/test/onChain/OnChain.fixture.ts +++ b/contracts/internal/host-chain/test/onChain/OnChain.fixture.ts @@ -1,5 +1,53 @@ import type { OnChain, OnChain2 } from "../../types"; import hre from "hardhat"; +const { ethers } = hre; + +// The hardcoded TaskManager address that ACL and PlaintextsStorage expect +const TASK_MANAGER_ADDRESS = "0xeA30c4B8b44078Bbf8a6ef5b9f1eC1626C7848D9"; + +/** + * Deploy a proxy at a specific address using hardhat_setCode + */ +async function deployProxyAtAddress( + targetAddress: string, + implementationAddress: string, + initData: string +): Promise { + const ERC1967Proxy = await ethers.getContractFactory("ERC1967Proxy"); + const tempProxy = await ERC1967Proxy.deploy(implementationAddress, initData); + await tempProxy.waitForDeployment(); + + const proxyBytecode = await ethers.provider.getCode(await tempProxy.getAddress()); + await ethers.provider.send("hardhat_setCode", [targetAddress, proxyBytecode]); + + // Storage slots to copy (ERC1967 + OZ v5 namespaced storage) + const storageSlots = [ + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", // ERC1967 implementation + "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00", // OZ Initializable + "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300", // OZ Ownable + "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199301", // OZ Ownable2Step pending + // TaskManager storage slots + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000000000000000000000000000003", + "0x0000000000000000000000000000000000000000000000000000000000000004", + "0x0000000000000000000000000000000000000000000000000000000000000005", + "0x0000000000000000000000000000000000000000000000000000000000000006", + "0x0000000000000000000000000000000000000000000000000000000000000007", + "0x0000000000000000000000000000000000000000000000000000000000000008", + "0x0000000000000000000000000000000000000000000000000000000000000009", + "0x000000000000000000000000000000000000000000000000000000000000000a", + ]; + + const tempAddress = await tempProxy.getAddress(); + for (const slot of storageSlots) { + const value = await ethers.provider.getStorage(tempAddress, slot); + if (value !== "0x0000000000000000000000000000000000000000000000000000000000000000") { + await ethers.provider.send("hardhat_setStorageAt", [targetAddress, slot, value]); + } + } +} export async function deployOnChainFixture(): Promise<{ testContract: OnChain; @@ -7,18 +55,54 @@ export async function deployOnChainFixture(): Promise<{ address: string; address2: string; }> { - await hre.run("deploy"); + const [owner] = await ethers.getSigners(); + + // Deploy TaskManager implementation + const TaskManager = await ethers.getContractFactory("TaskManager"); + const taskManagerImpl = await TaskManager.deploy(); + await taskManagerImpl.waitForDeployment(); - const accounts = await hre.ethers.getSigners(); - const contractOwner = accounts[0]; + // Prepare init data and deploy at hardcoded address + const initData = TaskManager.interface.encodeFunctionData("initialize", [owner.address]); + await deployProxyAtAddress(TASK_MANAGER_ADDRESS, await taskManagerImpl.getAddress(), initData); - const OnChain = await hre.ethers.getContractFactory("OnChain"); - const OnChain2 = await hre.ethers.getContractFactory("OnChain2"); - const testContract = await OnChain.connect(contractOwner).deploy(); + // Get TaskManager at the hardcoded address + const taskManager = TaskManager.attach(TASK_MANAGER_ADDRESS); + + // Deploy ACL + const ACL = await ethers.getContractFactory("ACL"); + const aclImpl = await ACL.deploy(); + await aclImpl.waitForDeployment(); + + const ERC1967Proxy = await ethers.getContractFactory("ERC1967Proxy"); + const aclInitData = ACL.interface.encodeFunctionData("initialize", [owner.address]); + const aclProxy = await ERC1967Proxy.deploy(await aclImpl.getAddress(), aclInitData); + await aclProxy.waitForDeployment(); + + // Deploy PlaintextsStorage + const PlaintextsStorage = await ethers.getContractFactory("PlaintextsStorage"); + const psImpl = await PlaintextsStorage.deploy(); + await psImpl.waitForDeployment(); + + const psInitData = PlaintextsStorage.interface.encodeFunctionData("initialize", [owner.address]); + const psProxy = await ERC1967Proxy.deploy(await psImpl.getAddress(), psInitData); + await psProxy.waitForDeployment(); + + // Configure TaskManager + await taskManager.setACLContract(await aclProxy.getAddress()); + await taskManager.setPlaintextsStorage(await psProxy.getAddress()); + await taskManager.setSecurityZones(-128, 127); + + // Deploy OnChain test contracts + const OnChain = await ethers.getContractFactory("OnChain"); + const OnChain2 = await ethers.getContractFactory("OnChain2"); + + const testContract = await OnChain.connect(owner).deploy(); await testContract.waitForDeployment(); - const testContract2 = await OnChain2.connect(contractOwner).deploy(); + const testContract2 = await OnChain2.connect(owner).deploy(); await testContract2.waitForDeployment(); + const address = await testContract.getAddress(); const address2 = await testContract2.getAddress(); @@ -26,14 +110,11 @@ export async function deployOnChainFixture(): Promise<{ } export async function getTokensFromFaucet() { + // No-op for Hardhat network - only needed for localfhenix if (hre.network.name === "localfhenix") { - const signers = await hre.ethers.getSigners(); - - if ( - (await hre.ethers.provider.getBalance(signers[0].address)).toString() === - "0" - ) { - await hre.fhenixjs.getFunds(signers[0].address); + const signers = await ethers.getSigners(); + if ((await ethers.provider.getBalance(signers[0].address)).toString() === "0") { + await (hre as any).fhenixjs.getFunds(signers[0].address); } } } diff --git a/contracts/internal/host-chain/test/publiclyAllowed/PubliclyAllowed.ts b/contracts/internal/host-chain/test/publiclyAllowed/PubliclyAllowed.ts new file mode 100644 index 0000000..a21fb2f --- /dev/null +++ b/contracts/internal/host-chain/test/publiclyAllowed/PubliclyAllowed.ts @@ -0,0 +1,108 @@ +import hre from "hardhat"; +import { expect } from "chai"; + +const { ethers } = hre; + +const TASK_MANAGER_ADDRESS = "0xeA30c4B8b44078Bbf8a6ef5b9f1eC1626C7848D9"; + +async function deployProxyAtAddress( + targetAddress: string, + implementationAddress: string, + initData: string +): Promise { + const ERC1967Proxy = await ethers.getContractFactory("ERC1967Proxy"); + const tempProxy = await ERC1967Proxy.deploy(implementationAddress, initData); + await tempProxy.waitForDeployment(); + + const proxyBytecode = await ethers.provider.getCode(await tempProxy.getAddress()); + await ethers.provider.send("hardhat_setCode", [targetAddress, proxyBytecode]); + + const storageSlots = [ + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00", + "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300", + "0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199301", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000000000000000000000000000003", + "0x0000000000000000000000000000000000000000000000000000000000000004", + "0x0000000000000000000000000000000000000000000000000000000000000005", + "0x0000000000000000000000000000000000000000000000000000000000000006", + "0x0000000000000000000000000000000000000000000000000000000000000007", + "0x0000000000000000000000000000000000000000000000000000000000000008", + "0x0000000000000000000000000000000000000000000000000000000000000009", + "0x000000000000000000000000000000000000000000000000000000000000000a", + ]; + + const tempAddress = await tempProxy.getAddress(); + for (const slot of storageSlots) { + const value = await ethers.provider.getStorage(tempAddress, slot); + if (value !== "0x0000000000000000000000000000000000000000000000000000000000000000") { + await ethers.provider.send("hardhat_setStorageAt", [targetAddress, slot, value]); + } + } +} + +describe("PubliclyAllowed Tests", function () { + let taskManager: any; + let testContract: any; + + before(async function () { + const [owner] = await ethers.getSigners(); + + const TaskManager = await ethers.getContractFactory("TaskManager"); + const taskManagerImpl = await TaskManager.deploy(); + await taskManagerImpl.waitForDeployment(); + + const initData = TaskManager.interface.encodeFunctionData("initialize", [owner.address]); + await deployProxyAtAddress(TASK_MANAGER_ADDRESS, await taskManagerImpl.getAddress(), initData); + taskManager = TaskManager.attach(TASK_MANAGER_ADDRESS); + + const ACL = await ethers.getContractFactory("ACL"); + const aclImpl = await ACL.deploy(); + await aclImpl.waitForDeployment(); + + const ERC1967Proxy = await ethers.getContractFactory("ERC1967Proxy"); + const aclInitData = ACL.interface.encodeFunctionData("initialize", [owner.address]); + const aclProxy = await ERC1967Proxy.deploy(await aclImpl.getAddress(), aclInitData); + await aclProxy.waitForDeployment(); + + const PlaintextsStorage = await ethers.getContractFactory("PlaintextsStorage"); + const psImpl = await PlaintextsStorage.deploy(); + await psImpl.waitForDeployment(); + const psInitData = PlaintextsStorage.interface.encodeFunctionData("initialize", [owner.address]); + const psProxy = await ERC1967Proxy.deploy(await psImpl.getAddress(), psInitData); + await psProxy.waitForDeployment(); + + await taskManager.setACLContract(await aclProxy.getAddress()); + await taskManager.setPlaintextsStorage(await psProxy.getAddress()); + await taskManager.setSecurityZones(-128, 127); + + const PubliclyAllowedTest = await ethers.getContractFactory("PubliclyAllowedTest"); + testContract = await PubliclyAllowedTest.connect(owner).deploy(); + await testContract.waitForDeployment(); + }); + + describe("isPubliclyAllowed", function () { + it("should return false for a handle that is not globally allowed", async function () { + const tx = await testContract.createWithoutGlobal(42); + await tx.wait(); + const handle = await testContract.lastHandle(); + expect(await taskManager.isPubliclyAllowed(handle)).to.equal(false); + }); + + it("should return true after allowGlobal is called", async function () { + const tx = await testContract.createAndAllowGlobal(99); + await tx.wait(); + const handle = await testContract.lastHandle(); + expect(await taskManager.isPubliclyAllowed(handle)).to.equal(true); + }); + + it("should return false for a non-existent handle", async function () { + const fakeHandle = 12345; + expect(await taskManager.isPubliclyAllowed(fakeHandle)).to.equal(false); + }); + }); + +}); diff --git a/contracts/internal/host-chain/test/types.ts b/contracts/internal/host-chain/test/types.ts index 6d9364a..c2c296b 100644 --- a/contracts/internal/host-chain/test/types.ts +++ b/contracts/internal/host-chain/test/types.ts @@ -1,4 +1,5 @@ import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; +import { Contract, Wallet } from "ethers"; type Fixture = () => Promise; @@ -6,6 +7,13 @@ declare module "mocha" { export interface Context { loadFixture: (fixture: Fixture) => Promise; signers: Signers; + // DecryptResult test context + taskManager?: Contract; + plaintextsStorage?: Contract; + owner?: HardhatEthersSigner; + testSigner?: Wallet; + otherAccount?: HardhatEthersSigner; + originalSigner?: string; } }