Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/sdk-mock-decrypt-error-codes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cofhe/sdk': patch
---

Decrypts on the mocks now fail with the same error codes as the threshold network: a rejected ACP throws `ACPDenied`, `ACPExpired` or `ACPInvalid`, and `decryptForTx().withoutACP()` on a value that is not public throws `NotPubliclyAllowed`, each with its `apiErrorCode`. Previously every mock rejection was a generic `SealOutputFailed` or `DecryptFailed`.
22 changes: 22 additions & 0 deletions packages/sdk/core/decrypt/apiError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,25 @@ export function mapApiErrorCodeToCofheErrorCode(
}
return fallback;
}

/**
* The backend code the threshold network answers with for a rejection reported by the mocks'
* `MockThresholdNetwork`, so decrypts on the mocks fail with the same `CofheErrorCode`. `undefined`
* for a reason it has no dedicated code for.
*/
export function mockRejectionToApiErrorCode(reason: string, withACP: boolean): BackendApiErrorCode | undefined {
if (!withACP) return reason === 'NotAllowed' ? 'not_publicly_allowed' : undefined;

switch (reason) {
case 'NotAllowed':
case 'PermissionInvalid_Disabled':
return 'acp_denied';
case 'PermissionInvalid_Expired':
return 'acp_expired';
case 'PermissionInvalid_IssuerSignature':
case 'PermissionInvalid_RecipientSignature':
return 'acp_invalid';
default:
return undefined;
}
}
16 changes: 6 additions & 10 deletions packages/sdk/core/decrypt/cofheMocksDecryptForTx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { FheTypes } from '../types.js';
import { CofheError, CofheErrorCode } from '../error.js';
import { MOCKS_DECRYPT_RESULT_SIGNER_PRIVATE_KEY } from '../consts.js';
import { MOCKS_THRESHOLD_NETWORK_ADDRESS } from '../consts.js';
import { mapApiErrorCodeToCofheErrorCode, mockRejectionToApiErrorCode } from './apiError.js';

const UINT_TYPE_MASK = 0x7fn;
const TYPE_BYTE_OFFSET = 8n;
Expand Down Expand Up @@ -54,17 +55,12 @@ export async function cofheMocksDecryptForTx(
});
}

if (error != '') {
if (error != '' || allowed == false) {
const apiErrorCode = mockRejectionToApiErrorCode(error || 'NotAllowed', acp !== null);
throw new CofheError({
code: CofheErrorCode.DecryptFailed,
message: `mocks decryptForTx call failed: ${error}`,
});
}

if (allowed == false) {
throw new CofheError({
code: CofheErrorCode.DecryptFailed,
message: `mocks decryptForTx call failed: ACL Access Denied (NotAllowed)`,
code: mapApiErrorCodeToCofheErrorCode(apiErrorCode, CofheErrorCode.DecryptFailed),
apiErrorCode,
message: `mocks decryptForTx call failed: ${error || 'ACL Access Denied (NotAllowed)'}`,
});
}

Expand Down
16 changes: 6 additions & 10 deletions packages/sdk/core/decrypt/cofheMocksDecryptForView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MockThresholdNetworkAbi } from './MockThresholdNetworkAbi.js';
import { FheTypes } from '../types.js';
import { CofheError, CofheErrorCode } from '../error.js';
import { MOCKS_THRESHOLD_NETWORK_ADDRESS } from '../consts.js';
import { mapApiErrorCodeToCofheErrorCode, mockRejectionToApiErrorCode } from './apiError.js';

export async function cofheMocksDecryptForView(
ctHash: bigint | string,
Expand All @@ -26,17 +27,12 @@ export async function cofheMocksDecryptForView(
args: [BigInt(ctHash), BigInt(utype), permissionWithBigInts],
});

if (error != '') {
if (error != '' || allowed == false) {
const apiErrorCode = mockRejectionToApiErrorCode(error || 'NotAllowed', true);
throw new CofheError({
code: CofheErrorCode.SealOutputFailed,
message: `mocks querySealOutput call failed: ${error}`,
});
}

if (allowed == false) {
throw new CofheError({
code: CofheErrorCode.SealOutputFailed,
message: `mocks querySealOutput call failed: ACL Access Denied (NotAllowed)`,
code: mapApiErrorCodeToCofheErrorCode(apiErrorCode, CofheErrorCode.SealOutputFailed),
apiErrorCode,
message: `mocks querySealOutput call failed: ${error || 'ACL Access Denied (NotAllowed)'}`,
});
}

Expand Down
77 changes: 77 additions & 0 deletions packages/sdk/core/test/cofheMocksDecryptErrors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, it, expect, vi } from 'vitest';
import type { PublicClient } from 'viem';
import { createMockACP } from '../../acps/test-utils.js';
import { cofheMocksDecryptForView } from '../decrypt/cofheMocksDecryptForView.js';
import { cofheMocksDecryptForTx } from '../decrypt/cofheMocksDecryptForTx.js';
import { CofheErrorCode } from '../error.js';
import { FheTypes } from '../types.js';

// Reasons MockThresholdNetwork reports for a rejected ACP, and the code the threshold network
// answers the same case with.
const ACP_REJECTIONS = [
{ reason: 'NotAllowed', code: CofheErrorCode.ACPDenied, apiErrorCode: 'acp_denied' },
{ reason: 'PermissionInvalid_Disabled', code: CofheErrorCode.ACPDenied, apiErrorCode: 'acp_denied' },
{ reason: 'PermissionInvalid_Expired', code: CofheErrorCode.ACPExpired, apiErrorCode: 'acp_expired' },
{ reason: 'PermissionInvalid_IssuerSignature', code: CofheErrorCode.ACPInvalid, apiErrorCode: 'acp_invalid' },
{ reason: 'PermissionInvalid_RecipientSignature', code: CofheErrorCode.ACPInvalid, apiErrorCode: 'acp_invalid' },
];

const publicClientReturning = (result: readonly [boolean, string, bigint | `0x${string}`]) =>
({ chain: { id: 31337 }, readContract: vi.fn(async () => result) }) as unknown as PublicClient;

describe('mocks decryptForView errors', () => {
it.each(ACP_REJECTIONS)('maps $reason to $apiErrorCode', async ({ reason, code, apiErrorCode }) => {
const publicClient = publicClientReturning([false, reason, '0x00']);

await expect(
cofheMocksDecryptForView(1n, FheTypes.Uint32, await createMockACP(), publicClient)
).rejects.toMatchObject({
code,
apiErrorCode,
message: expect.stringContaining(reason),
});
});

it('keeps SealOutputFailed for a reason the threshold network has no code for', async () => {
const publicClient = publicClientReturning([false, 'Low Level Error', '0x00']);

await expect(
cofheMocksDecryptForView(1n, FheTypes.Uint32, await createMockACP(), publicClient)
).rejects.toMatchObject({
code: CofheErrorCode.SealOutputFailed,
apiErrorCode: undefined,
});
});
});

describe('mocks decryptForTx errors', () => {
it.each(ACP_REJECTIONS)('with an ACP, maps $reason to $apiErrorCode', async ({ reason, code, apiErrorCode }) => {
const publicClient = publicClientReturning([false, reason, 0n]);

await expect(
cofheMocksDecryptForTx(1n, FheTypes.Uint32, await createMockACP(), publicClient)
).rejects.toMatchObject({
code,
apiErrorCode,
message: expect.stringContaining(reason),
});
});

it('without an ACP, maps NotAllowed to not_publicly_allowed', async () => {
const publicClient = publicClientReturning([false, 'NotAllowed', 0n]);

await expect(cofheMocksDecryptForTx(1n, FheTypes.Uint32, null, publicClient)).rejects.toMatchObject({
code: CofheErrorCode.NotPubliclyAllowed,
apiErrorCode: 'not_publicly_allowed',
});
});

it('keeps DecryptFailed for a reason the threshold network has no code for', async () => {
const publicClient = publicClientReturning([false, 'Panic', 0n]);

await expect(cofheMocksDecryptForTx(1n, FheTypes.Uint32, null, publicClient)).rejects.toMatchObject({
code: CofheErrorCode.DecryptFailed,
apiErrorCode: undefined,
});
});
});
24 changes: 23 additions & 1 deletion test/hardhat-3-plugin-test/test/inherited.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { network } from 'hardhat';
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { Encryptable, FheTypes } from '@cofhe/sdk';
import { CofheErrorCode, Encryptable, FheTypes } from '@cofhe/sdk';

const ALICE_PRIVATE_KEY = '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d' as const;

Expand Down Expand Up @@ -74,6 +74,28 @@ describe('Inherited SDK Tests', async () => {
assert.equal(Number(publishedValue), Number(testValue));
});

it('decryptForView with an ACP that does not cover the value → ACPDenied', async () => {
const client = await cofhe.createClientWithBatteries(bobWalletClient);
const { ctHash } = await storeEncrypted(client, 7n);
const [bobAddress] = await bobWalletClient.getAddresses();
const acp = await client.acp.createSelf({ issuer: bobAddress, contracts: [aliceAccount.address] });

await assert.rejects(client.decryptForView(ctHash, FheTypes.Uint32).withACP(acp).execute(), {
code: CofheErrorCode.ACPDenied,
apiErrorCode: 'acp_denied',
});
});

it('decryptForTx without an ACP on a value that is not public → NotPubliclyAllowed', async () => {
const client = await cofhe.createClientWithBatteries(bobWalletClient);
const { ctHash } = await storeEncrypted(client, 8n);

await assert.rejects(client.decryptForTx(ctHash).withoutACP().execute(), {
code: CofheErrorCode.NotPubliclyAllowed,
apiErrorCode: 'not_publicly_allowed',
});
});

it('self acp: create → verify active', async () => {
const client = await cofhe.createClientWithBatteries(bobWalletClient);
const [bobAddress] = await bobWalletClient.getAddresses();
Expand Down