Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
024fc43
fix: foundry tests
alexshchur Feb 24, 2026
f423eb3
tests: decrypt-with-proof draft
alexshchur Feb 24, 2026
ee3c7da
test: replace foundry test with hre plugin test
alexshchur Feb 24, 2026
ba5be69
chore: couple annotations
alexshchur Feb 25, 2026
2a7add1
fix: skip base integration test if it's the default PK
alexshchur Feb 25, 2026
b960114
tmp: switch to local @fhenixprotocol/cofhe-contracts to reach a subfo…
alexshchur Feb 25, 2026
bfd4c1b
fix: cast uwrap results to uint256 and drop euint256 support
alexshchur Feb 25, 2026
aea164c
fix: add new fn implementations for MockTaskManager
alexshchur Feb 25, 2026
c35495c
chore: update artefacts
alexshchur Feb 25, 2026
15f8646
chore: high-level section for mock-contracts
alexshchur Feb 25, 2026
b0b5d35
tmp: testBed annotations
alexshchur Feb 25, 2026
baecc82
tmp: annotations for TestBed.sol and TestBed.t.sol
alexshchur Feb 25, 2026
6b03797
chore: MockQueryDecrypter -> MockThresholdNetwork
alexshchur Feb 25, 2026
81e057d
test: involve TestBed into publishDecryptResult
alexshchur Feb 25, 2026
cbdf62d
test: involve TestBed into publishDecryptResult
alexshchur Feb 25, 2026
cb4717f
chore: prepare for decryptForTx implementation (untested yet)
alexshchur Feb 25, 2026
9b3172b
feat: add DecryptForTxBuilder and decryptForTx client bindings (mock …
alexshchur Feb 25, 2026
cd6f03f
tests: client.decryptForTx mocks
alexshchur Feb 25, 2026
1016f2a
test: FHE(a+b) + decryptForTx + publishDecryptResult + verify decrypt…
alexshchur Feb 25, 2026
25aca41
chore: centralize the signer used for setDecryptResultSigner
alexshchur Feb 25, 2026
5486ec2
tests: when trying to fetch without permit what is not global - error
alexshchur Feb 25, 2026
cfa2b0b
test: add a successful test without permit (public value)
alexshchur Feb 25, 2026
67697e4
chore: better naming for tests
alexshchur Feb 25, 2026
a678ea7
chore: rename decryptHandle to decryptForView
alexshchur Feb 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions packages/hardhat-plugin-test/contracts/SimpleTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,45 @@ import '@fhenixprotocol/cofhe-contracts/FHE.sol';
contract SimpleTest {
euint32 public storedValue;
uint256 public storedValueHash;
euint32 public publicValue;
uint256 public publicValueHash;

function setValueTrivial(uint256 inValue) public {
storedValue = FHE.asEuint32(inValue);
storedValueHash = euint32.unwrap(storedValue);
storedValueHash = uint256(euint32.unwrap(storedValue));
FHE.allowThis(storedValue);
FHE.allowSender(storedValue);
}

/**
* Set a globally accessible encrypted value (everyone can decrypt)
* @param inValue The encrypted value to set globally
*/
function setPublicValue(InEuint32 memory inValue) public {
publicValue = FHE.asEuint32(inValue);
publicValueHash = uint256(euint32.unwrap(publicValue));
FHE.allowPublic(publicValue);
}

/**
* Store an encrypted value
* @param inValue The encrypted value to store
*/
function setValue(InEuint32 memory inValue) public {
storedValue = FHE.asEuint32(inValue);
storedValueHash = euint32.unwrap(storedValue);
storedValueHash = uint256(euint32.unwrap(storedValue));
FHE.allowThis(storedValue);
FHE.allowSender(storedValue);
}

/**
* Add an encrypted value to the stored value
* @param inValue The encrypted value to add
*/
function addValue(InEuint32 memory inValue) public {
euint32 valueToAdd = FHE.asEuint32(inValue);
storedValue = FHE.add(storedValue, valueToAdd);
storedValueHash = uint256(euint32.unwrap(storedValue));
FHE.allowThis(storedValue);
FHE.allowSender(storedValue);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/hardhat-plugin-test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@cofhe/hardhat-plugin(tests)",
"private": true,
"scripts": {
"test": "npx hardhat test",
"test": "pnpm -C ../sdk build && npx hardhat test",
"test:integration": "npx hardhat test test/integration-*.test.ts",
"compile": "npx hardhat compile",
"clean": "npx hardhat clean"
Expand All @@ -15,7 +15,7 @@
"@cofhe/sdk": "workspace:*",
"@cofhe/hardhat-plugin": "workspace:*",
"@cofhe/mock-contracts": "workspace:*",
"@fhenixprotocol/cofhe-contracts": "0.0.13",
"@fhenixprotocol/cofhe-contracts": "link:../../../cofhe-contracts/contracts",
"@openzeppelin/contracts": "^5.0.0",
"viem": "^2.38.6",
"dotenv": "^16.0.0"
Expand Down
178 changes: 178 additions & 0 deletions packages/hardhat-plugin-test/test/decryptForTx-builder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import hre from 'hardhat';
import { CofheClient, Encryptable, FheTypes } from '@cofhe/sdk';
import { TASK_COFHE_MOCKS_DEPLOY } from './consts';
import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers';
import { expect } from 'chai';
import { PermitUtils } from '@cofhe/sdk/permits';

describe('DecryptForTx Integration Tests', () => {
let cofheClient: CofheClient;
let testContract: any;
let signer: HardhatEthersSigner;

before(async function () {
await hre.run(TASK_COFHE_MOCKS_DEPLOY);
const [tmpSigner] = await hre.ethers.getSigners();
signer = tmpSigner;
cofheClient = await hre.cofhe.createClientWithBatteries(signer);

const SimpleTest = await hre.ethers.getContractFactory('SimpleTest');
testContract = await SimpleTest.deploy();
await testContract.waitForDeployment();
console.log(`Test contract deployed at: ${await testContract.getAddress()}`);
});

describe('decryptForTx with global allowance', () => {
it('Should fail to decrypt without a permit when not globally allowed', async function () {
const testValue = 42n;
const encrypted = await cofheClient.encryptInputs([Encryptable.uint32(testValue)]).execute();
const tx = await testContract.connect(signer).setValue(encrypted[0]);
await tx.wait();

try {
await cofheClient.decryptForTx(encrypted[0].ctHash).withPermit(undefined).execute();
expect.fail('Expected decryptForTx to fail without global allowance');
} catch (error) {
expect((error as Error).message).to.include('NotAllowed');
}
});

it('Should successfully decrypt without a permit when globally allowed', async function () {
const testValue = 55n;
const encrypted = await cofheClient.encryptInputs([Encryptable.uint32(testValue)]).execute();
const tx = await testContract.connect(signer).setPublicValue(encrypted[0]);
await tx.wait();

const result = await cofheClient.decryptForTx(encrypted[0].ctHash).withPermit(undefined).execute();

expect(result.ctHash).to.be.equal(encrypted[0].ctHash);
expect(result.decryptedValue).to.be.equal(testValue);
expect(result.signature).to.be.a('string');
});

it('Should decrypt different values consistently with a permit', async function () {
const testValues = [1n, 100n, 1000n, 65535n];
const permit = await cofheClient.permits.createSelf({
issuer: signer.address,
name: 'Consistency Permit',
});

for (const testValue of testValues) {
const encrypted = await cofheClient.encryptInputs([Encryptable.uint32(testValue)]).execute();
const tx = await testContract.connect(signer).setValue(encrypted[0]);
await tx.wait();

const result = await cofheClient.decryptForTx(encrypted[0].ctHash).withPermit(permit).execute();
expect(result.ctHash).to.be.equal(encrypted[0].ctHash);
expect(result.decryptedValue).to.be.equal(testValue);
expect(result.signature).to.be.a('string');
}
});
});

describe('decryptForTx with permit', () => {
it('Should decrypt with a self permit', async function () {
const testValue = 99n;
const permit = await cofheClient.permits.createSelf({
issuer: signer.address,
name: 'Test Permit',
});

const encrypted = await cofheClient.encryptInputs([Encryptable.uint32(testValue)]).execute();
const tx = await testContract.connect(signer).setValue(encrypted[0]);
await tx.wait();

const result = await cofheClient.decryptForTx(encrypted[0].ctHash).withPermit(permit).execute();

expect(result.ctHash).to.be.equal(encrypted[0].ctHash);
expect(result.decryptedValue).to.be.equal(testValue);
expect(result.signature).to.be.a('string');
});

it('Should auto-resolve active permit', async function () {
const testValue = 88n;
const permit = await cofheClient.permits.createSelf({
issuer: signer.address,
name: 'Active Test Permit',
});
const permitHash = PermitUtils.getHash(permit);
cofheClient.permits.selectActivePermit(permitHash);

const encrypted = await cofheClient.encryptInputs([Encryptable.uint32(testValue)]).execute();
const tx = await testContract.connect(signer).setValue(encrypted[0]);
await tx.wait();

const result = await cofheClient.decryptForTx(encrypted[0].ctHash).execute();

expect(result.ctHash).to.be.equal(encrypted[0].ctHash);
expect(result.decryptedValue).to.be.equal(testValue);
expect(result.signature).to.be.a('string');
});
});

describe('decryptForTx builder chain', () => {
it('Should support builder chaining', async function () {
const testValue = 33n;
const encrypted = await cofheClient.encryptInputs([Encryptable.uint32(testValue)]).execute();
const tx = await testContract.connect(signer).setValue(encrypted[0]);
await tx.wait();

const result = await cofheClient.decryptForTx(encrypted[0].ctHash).setAccount(signer.address).execute();

expect(result.ctHash).to.be.equal(encrypted[0].ctHash);
expect(result.decryptedValue).to.be.equal(testValue);
expect(result.signature).to.be.a('string');
});

it('Should maintain builder state across multiple calls', async function () {
const testValue = 22n;
const encrypted = await cofheClient.encryptInputs([Encryptable.uint32(testValue)]).execute();
const tx = await testContract.connect(signer).setValue(encrypted[0]);
await tx.wait();

const builder = cofheClient.decryptForTx(encrypted[0].ctHash).setAccount(signer.address);

const result1 = await builder.execute();
expect(result1.ctHash).to.be.equal(encrypted[0].ctHash);
expect(result1.decryptedValue).to.be.equal(testValue);

const result2 = await builder.execute();
expect(result2.ctHash).to.be.equal(encrypted[0].ctHash);
expect(result2.decryptedValue).to.be.equal(testValue);
});
});

describe('decryptForTx error cases', () => {
// Error handling tests - can be extended as needed
});

describe('decryptForTx vs decryptForView', () => {
it('Should return plaintext value', async function () {
const testValue = 123n;
const encrypted = await cofheClient.encryptInputs([Encryptable.uint32(testValue)]).execute();
const tx = await testContract.connect(signer).setValue(encrypted[0]);
await tx.wait();

const decryptForTxResult = await cofheClient.decryptForTx(encrypted[0].ctHash).execute();

expect(decryptForTxResult.ctHash).to.be.equal(encrypted[0].ctHash);
expect(decryptForTxResult.decryptedValue).to.be.equal(testValue);
expect(typeof decryptForTxResult.signature).to.equal('string');
});

it('Should support both decryptForView and decryptForTx', async function () {
const testValue = 234n;
const encrypted = await cofheClient.encryptInputs([Encryptable.uint32(testValue)]).execute();
const tx = await testContract.connect(signer).setValue(encrypted[0]);
await tx.wait();

const ctHash = encrypted[0].ctHash;
const viewResult = await cofheClient.decryptForView(ctHash, FheTypes.Uint32).execute();
const forTxResult = await cofheClient.decryptForTx(ctHash).execute();

expect(viewResult).to.be.equal(testValue);
expect(forTxResult.ctHash).to.be.equal(ctHash);
expect(forTxResult.decryptedValue).to.be.equal(testValue);
});
});
});
79 changes: 79 additions & 0 deletions packages/hardhat-plugin-test/test/decryptForTx-publish.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import hre from 'hardhat';
import { expect } from 'chai';
import { TASK_COFHE_MOCKS_DEPLOY } from './consts';
import { CofheClient, Encryptable, MOCKS_DECRYPT_RESULT_SIGNER_PRIVATE_KEY } from '@cofhe/sdk';
import { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers';
import { Wallet } from 'ethers';

// Test the full workflow:
// 1. Encrypt values and submit on-chain
// 2. Perform FHE operation (add)
// 3. Use decryptForTx to get ctHash, decryptedValue, and signature
// 4. Call publishDecryptResult with those values
// 5. Verify with getDecryptResultSafe

describe('Decrypt With Proof Test', () => {
let cofheClient: CofheClient;
let testContract: any;
let signer: HardhatEthersSigner;
let testBed: any;

before(async function () {
await hre.run(TASK_COFHE_MOCKS_DEPLOY);
const [tmpSigner] = await hre.ethers.getSigners();
signer = tmpSigner;
cofheClient = await hre.cofhe.createClientWithBatteries(signer);

// Deploy test contract for FHE operations
const SimpleTest = await hre.ethers.getContractFactory('SimpleTest');
testContract = await SimpleTest.deploy();
await testContract.waitForDeployment();

testBed = await hre.cofhe.mocks.getTestBed();
});

it('Should encrypt, compute FHE operation, decrypt, and publish result', async function () {
// Step 1: Encrypt two values
const valueX = 100n;
const valueY = 50n;
const expectedSum = valueX + valueY; // 150

const [encX, encY] = await cofheClient
.encryptInputs([Encryptable.uint32(valueX), Encryptable.uint32(valueY)])
.execute();

// Step 2: Submit encrypted values on-chain and perform FHE.add
const tx1 = await testContract.connect(signer).setValue(encX);
await tx1.wait();

const tx2 = await testContract.connect(signer).addValue(encY);
await tx2.wait();

const resultCtHash = await testContract.getValue();

// Step 3: Use decryptForTx to get the values needed for publishDecryptResult
const decryptResult = await cofheClient.decryptForTx(resultCtHash).execute();

expect(decryptResult.ctHash).to.equal(resultCtHash);
expect(decryptResult.decryptedValue).to.equal(expectedSum);
expect(decryptResult.signature).to.be.a('string');

// Step 4: Publish the decrypt result on-chain
// Configure the mock task manager to accept the mock signature
const taskManager = await hre.cofhe.mocks.getMockTaskManager();
const messageSigner = new Wallet(MOCKS_DECRYPT_RESULT_SIGNER_PRIVATE_KEY);
const signature = `0x${decryptResult.signature}`;
const setSignerTx = await taskManager.connect(signer).setDecryptResultSigner(messageSigner.address);
await setSignerTx.wait();

// Publish the decrypt result on-chain
// publishDecryptResult expects (euint32, uint32, bytes signature)
const publishTx = await testBed.publishDecryptResult(decryptResult.ctHash, decryptResult.decryptedValue, signature);
await publishTx.wait();

// Step 5: Verify the published result
const [publishedValue, isDecrypted] = await testBed.getDecryptResultSafe(decryptResult.ctHash);
expect(isDecrypted).to.equal(true);
expect(publishedValue).to.equal(expectedSum);
});
});
6 changes: 3 additions & 3 deletions packages/hardhat-plugin-test/test/deploy-mocks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ describe('Deploy Mocks Task', () => {

// QUERY DECRYPTER

const queryDecrypterFromCofhesdk = await hre.cofhe.mocks.getMockQueryDecrypter();
expect(await queryDecrypterFromCofhesdk.exists()).to.be.true;
expect(await queryDecrypterFromCofhesdk.getAddress()).to.be.equal(MOCKS_QUERY_DECRYPTER_ADDRESS);
const thresholdNetworkFromCofhesdk = await hre.cofhe.mocks.getMockThresholdNetwork();
expect(await thresholdNetworkFromCofhesdk.exists()).to.be.true;
expect(await thresholdNetworkFromCofhesdk.getAddress()).to.be.equal(MOCKS_QUERY_DECRYPTER_ADDRESS);

// TEST BED

Expand Down
2 changes: 1 addition & 1 deletion packages/hardhat-plugin-test/test/encrypt-inputs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('Encrypt Inputs Test', () => {
const ctHash = await testBed.numberHash();

// Decrypt number from TestBed
const unsealed = await client.decryptHandle(ctHash, FheTypes.Uint32).execute();
const unsealed = await client.decryptForView(ctHash, FheTypes.Uint32).execute();

expect(unsealed).to.be.equal(7n);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import { PermitUtils } from '@cofhe/sdk/permits';

// Test private key - should be funded on Base Sepolia
// Using a well-known test key, but you'll need to fund it with testnet ETH
const DEFAULT_TEST_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
const TEST_PRIVATE_KEY =
process.env.TEST_PRIVATE_KEY || '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
process.env.TEST_PRIVATE_KEY ||
DEFAULT_TEST_PRIVATE_KEY; /* This key is publicly known and should only be used for testing with testnet ETH. Do not use this key on mainnet or with real funds. */

const deployments = {
[baseSepolia.id]: {
Expand All @@ -29,7 +31,7 @@ describe('Base Sepolia Integration Tests', () => {

before(async function () {
// Skip if no private key is provided (for CI/CD)
if (!process.env.BASE_SEPOLIA_PRIVATE_KEY && process.env.CI) {
if (TEST_PRIVATE_KEY === DEFAULT_TEST_PRIVATE_KEY) {
this.skip();
}

Expand Down Expand Up @@ -90,7 +92,7 @@ describe('Base Sepolia Integration Tests', () => {

it('Should encrypt -> store -> decrypt a value', async function () {
// Skip if no private key is provided
if (!process.env.BASE_SEPOLIA_PRIVATE_KEY && process.env.CI) {
if (TEST_PRIVATE_KEY === DEFAULT_TEST_PRIVATE_KEY) {
this.skip();
}

Expand All @@ -107,7 +109,7 @@ describe('Base Sepolia Integration Tests', () => {
const ctHash = await testContract.getValueHash();

// Decrypt the value using the ctHash from the encrypted input
const unsealedResult = await cofheClient.decryptHandle(ctHash, FheTypes.Uint32).execute();
const unsealedResult = await cofheClient.decryptForView(ctHash, FheTypes.Uint32).execute();

// Verify the decrypted value matches
expect(unsealedResult).to.be.equal(testValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('Hardhat Integration Tests', () => {
await tx.wait();

// Decrypt the value using the ctHash from the encrypted input
const unsealedResult = await cofheClient.decryptHandle(encrypted[0].ctHash, FheTypes.Uint32).execute();
const unsealedResult = await cofheClient.decryptForView(encrypted[0].ctHash, FheTypes.Uint32).execute();

// Verify the decrypted value matches
expect(unsealedResult).to.be.equal(testValue);
Expand Down
Loading
Loading