From 2d6d92eea06ccdcc87496cb9ff9b0785b421c52a Mon Sep 17 00:00:00 2001
From: Piotr Roslaniec
Date: Mon, 14 Aug 2023 11:16:22 +0200
Subject: [PATCH] draft taco api
---
src/characters/cbd-recipient.ts | 17 ++--
src/conditions/condition-expr.ts | 5 ++
src/dkg.ts | 13 +++
src/sdk/strategy/cbd-strategy.ts | 6 +-
src/taco.ts | 61 ++++++++++++++
test/unit/cbd-strategy.test.ts | 2 +-
test/unit/conditions/condition-expr.test.ts | 33 +++++---
test/unit/taco.test.ts | 88 +++++++++++++++++++++
test/utils.ts | 5 +-
9 files changed, 203 insertions(+), 27 deletions(-)
create mode 100644 src/taco.ts
create mode 100644 test/unit/taco.test.ts
diff --git a/src/characters/cbd-recipient.ts b/src/characters/cbd-recipient.ts
index c37629fdc..cb5a2696a 100644
--- a/src/characters/cbd-recipient.ts
+++ b/src/characters/cbd-recipient.ts
@@ -15,7 +15,6 @@ import { ethers } from 'ethers';
import { DkgCoordinatorAgent, DkgParticipant } from '../agents/coordinator';
import { ConditionExpression } from '../conditions';
-import { DkgRitual } from '../dkg';
import { PorterClient } from '../porter';
import { fromJSON, toJSON } from '../utils';
@@ -34,22 +33,22 @@ export class ThresholdDecrypter {
private readonly threshold: number
) {}
- public static create(porterUri: string, dkgRitual: DkgRitual) {
+ public static create(porterUri: string, ritualId: number, threshold: number) {
return new ThresholdDecrypter(
new PorterClient(porterUri),
- dkgRitual.id,
- dkgRitual.dkgParams.threshold
+ ritualId,
+ threshold
);
}
// Retrieve and decrypt ciphertext using provider and condition expression
public async retrieveAndDecrypt(
- provider: ethers.providers.Web3Provider,
+ web3Provider: ethers.providers.Web3Provider,
conditionExpr: ConditionExpression,
ciphertext: Ciphertext
): Promise {
const decryptionShares = await this.retrieve(
- provider,
+ web3Provider,
conditionExpr,
ciphertext
);
@@ -64,15 +63,15 @@ export class ThresholdDecrypter {
// Retrieve decryption shares
public async retrieve(
- provider: ethers.providers.Web3Provider,
+ web3Provider: ethers.providers.Web3Provider,
conditionExpr: ConditionExpression,
ciphertext: Ciphertext
): Promise {
const dkgParticipants = await DkgCoordinatorAgent.getParticipants(
- provider,
+ web3Provider,
this.ritualId
);
- const contextStr = await conditionExpr.buildContext(provider).toJson();
+ const contextStr = await conditionExpr.buildContext(web3Provider).toJson();
const { sharedSecrets, encryptedRequests } = this.makeDecryptionRequests(
this.ritualId,
ciphertext,
diff --git a/src/conditions/condition-expr.ts b/src/conditions/condition-expr.ts
index 61c446e53..6af48fb36 100644
--- a/src/conditions/condition-expr.ts
+++ b/src/conditions/condition-expr.ts
@@ -2,6 +2,7 @@ import { Conditions as WASMConditions } from '@nucypher/nucypher-core';
import { ethers } from 'ethers';
import { SemVer } from 'semver';
+import { fromBytes } from '../../test/utils';
import { objectEquals, toBytes, toJSON } from '../utils';
import {
@@ -94,6 +95,10 @@ export class ConditionExpression {
return toBytes(this.toJson());
}
+ public static fromAad(aad: Uint8Array): ConditionExpression {
+ return ConditionExpression.fromJSON(fromBytes(aad));
+ }
+
public equals(other: ConditionExpression): boolean {
return [
this.version === other.version,
diff --git a/src/dkg.ts b/src/dkg.ts
index e65095838..e1e0d59e6 100644
--- a/src/dkg.ts
+++ b/src/dkg.ts
@@ -133,6 +133,19 @@ export class DkgClient {
);
}
+ public static async getFinalizedRitual(
+ web3Provider: ethers.providers.Web3Provider,
+ ritualId: number
+ ): Promise {
+ const ritual = await DkgClient.getExistingRitual(web3Provider, ritualId);
+ if (ritual.state !== DkgRitualState.FINALIZED) {
+ throw new Error(
+ `Ritual ${ritualId} is not finalized. State: ${ritual.state}`
+ );
+ }
+ return ritual;
+ }
+
// TODO: Without Validator public key in Coordinator, we cannot verify the
// transcript. We need to add it to the Coordinator (nucypher-contracts #77).
// public async verifyRitual(ritualId: number): Promise {
diff --git a/src/sdk/strategy/cbd-strategy.ts b/src/sdk/strategy/cbd-strategy.ts
index 83f61365d..32bef0523 100644
--- a/src/sdk/strategy/cbd-strategy.ts
+++ b/src/sdk/strategy/cbd-strategy.ts
@@ -77,7 +77,11 @@ export class DeployedCbdStrategy {
) {}
public static create(dkgRitual: DkgRitual, porterUri: string) {
- const decrypter = ThresholdDecrypter.create(porterUri, dkgRitual);
+ const decrypter = ThresholdDecrypter.create(
+ porterUri,
+ dkgRitual.id,
+ dkgRitual.dkgParams.threshold
+ );
return new DeployedCbdStrategy(decrypter, dkgRitual.dkgPublicKey);
}
diff --git a/src/taco.ts b/src/taco.ts
new file mode 100644
index 000000000..3d4228c24
--- /dev/null
+++ b/src/taco.ts
@@ -0,0 +1,61 @@
+import { Ciphertext, ferveoEncrypt } from '@nucypher/nucypher-core';
+import { ethers } from 'ethers';
+
+import { ThresholdDecrypter } from './characters/cbd-recipient';
+import { ConditionExpression } from './conditions';
+import { DkgClient } from './dkg';
+import { toBytes } from './utils';
+
+export interface TacoMessageKit {
+ ciphertext: Ciphertext;
+ aad: Uint8Array;
+ ritualId: number;
+ threshold: number;
+}
+
+export const encrypt = async (
+ web3Provider: ethers.providers.Web3Provider,
+ message: string,
+ conditions: ConditionExpression,
+ ritualId: number
+): Promise => {
+ const dkgRitual = await DkgClient.getFinalizedRitual(web3Provider, ritualId);
+ const aad = conditions.asAad();
+ const ciphertext = ferveoEncrypt(
+ toBytes(message),
+ aad,
+ dkgRitual.dkgPublicKey
+ );
+ return {
+ ciphertext,
+ aad,
+ ritualId,
+ threshold: dkgRitual.dkgParams.threshold,
+ };
+};
+
+export const decrypt = async (
+ web3Provider: ethers.providers.Web3Provider,
+ messageKit: TacoMessageKit,
+ porterUri: string
+): Promise => {
+ const decrypter = ThresholdDecrypter.create(
+ porterUri,
+ messageKit.ritualId,
+ messageKit.threshold
+ );
+ const condExpr = ConditionExpression.fromAad(messageKit.aad);
+ // TODO: Need web3Provider to fetch participants from Coordinator to make decryption requests.
+ // Should we put them into the message kit instead?
+ // Consider case where participants are changing over time. Is that an issue we should consider now?
+ return decrypter.retrieveAndDecrypt(
+ web3Provider,
+ condExpr,
+ messageKit.ciphertext
+ );
+};
+
+export const taco = {
+ encrypt,
+ decrypt,
+};
diff --git a/test/unit/cbd-strategy.test.ts b/test/unit/cbd-strategy.test.ts
index f054bd15d..94c9db141 100644
--- a/test/unit/cbd-strategy.test.ts
+++ b/test/unit/cbd-strategy.test.ts
@@ -50,13 +50,13 @@ const makeCbdStrategy = async () => {
async function makeDeployedCbdStrategy() {
const strategy = await makeCbdStrategy();
-
const mockedDkg = fakeDkgFlow(variant, 0, 4, 4);
const mockedDkgRitual = fakeDkgRitual(mockedDkg);
const web3Provider = fakeWeb3Provider(aliceSecretKey.toBEBytes());
const getUrsulasSpy = mockGetUrsulas(ursulas);
const initializeRitualSpy = mockInitializeRitual(ritualId);
const getExistingRitualSpy = mockGetExistingRitual(mockedDkgRitual);
+
const deployedStrategy = await strategy.deploy(web3Provider);
expect(getUrsulasSpy).toHaveBeenCalled();
diff --git a/test/unit/conditions/condition-expr.test.ts b/test/unit/conditions/condition-expr.test.ts
index 806269a7f..4d961026f 100644
--- a/test/unit/conditions/condition-expr.test.ts
+++ b/test/unit/conditions/condition-expr.test.ts
@@ -77,7 +77,7 @@ describe('condition set', () => {
).toBeTruthy();
});
- it('different minor/patch version but same condition', async () => {
+ it('different minor/patch version but same condition', () => {
const conditionExprOlderMinorVersion = new ConditionExpression(
rpcCondition,
'0.1.0'
@@ -97,7 +97,7 @@ describe('condition set', () => {
).not.toBeTruthy();
});
- it('minor/patch number greater than major; still older', async () => {
+ it('minor/patch number greater than major; still older', () => {
const conditionExprOlderMinorVersion = new ConditionExpression(
rpcCondition,
'0.9.0'
@@ -140,7 +140,7 @@ describe('condition set', () => {
contractConditionWithAbi,
timeCondition,
compoundCondition,
- ])('same version but different condition', async (condition) => {
+ ])('same version but different condition', (condition) => {
const conditionExprSameVersionDifferentCondition =
new ConditionExpression(condition);
expect(
@@ -150,7 +150,7 @@ describe('condition set', () => {
).not.toBeTruthy();
});
- it('same contract condition although using erc721 helper', async () => {
+ it('same contract condition although using erc721 helper', () => {
const erc721ConditionExpr = new ConditionExpression(
erc721BalanceCondition
);
@@ -171,7 +171,7 @@ describe('condition set', () => {
rpcCondition,
timeCondition,
compoundCondition,
- ])('serializes to and from json', async (condition) => {
+ ])('serializes to and from json', (condition) => {
const conditionExpr = new ConditionExpression(condition);
const conditionExprJson = conditionExpr.toJson();
expect(conditionExprJson).toBeDefined();
@@ -186,7 +186,16 @@ describe('condition set', () => {
expect(conditionExprFromJson.equals(conditionExprFromJson)).toBeTruthy();
});
- it('incompatible version', async () => {
+ it('serializes to and from aad', () => {
+ const conditionExpr = new ConditionExpression(rpcCondition);
+ const conditionExprAad = conditionExpr.asAad();
+ const conditionExprFromAad =
+ ConditionExpression.fromAad(conditionExprAad);
+ expect(conditionExprFromAad).toBeDefined();
+ expect(conditionExprFromAad.equals(conditionExpr)).toBeTruthy();
+ });
+
+ it('incompatible version', () => {
const currentVersion = new SemVer(ConditionExpression.VERSION);
const invalidVersion = currentVersion.inc('major');
expect(() => {
@@ -249,13 +258,13 @@ describe('condition set', () => {
method: 'isPolicyActive',
},
},
- ])("can't determine condition type", async (invalidCondition) => {
+ ])("can't determine condition type", (invalidCondition) => {
expect(() => {
ConditionExpression.fromObj(invalidCondition);
}).toThrow('unrecognized condition data');
});
- it('erc721 condition serialization', async () => {
+ it('erc721 condition serialization', () => {
const conditionExpr = new ConditionExpression(erc721BalanceCondition);
const erc721BalanceConditionObj = erc721BalanceCondition.toObj();
@@ -283,7 +292,7 @@ describe('condition set', () => {
expect(conditionExprFromJson.condition).toBeInstanceOf(ContractCondition);
});
- it('contract condition no abi serialization', async () => {
+ it('contract condition no abi serialization', () => {
const conditionExpr = new ConditionExpression(contractConditionNoAbi);
const conditionExprJson = conditionExpr.toJson();
@@ -315,7 +324,7 @@ describe('condition set', () => {
expect(conditionExprFromJson.condition).toBeInstanceOf(ContractCondition);
});
- it('contract condition with abi serialization', async () => {
+ it('contract condition with abi serialization', () => {
const conditionExpr = new ConditionExpression(contractConditionWithAbi);
const conditionExprJson = conditionExpr.toJson();
@@ -348,7 +357,7 @@ describe('condition set', () => {
expect(conditionExprFromJson.condition).toBeInstanceOf(ContractCondition);
});
- it('time condition serialization', async () => {
+ it('time condition serialization', () => {
const conditionExpr = new ConditionExpression(timeCondition);
const conditionExprJson = conditionExpr.toJson();
@@ -371,7 +380,7 @@ describe('condition set', () => {
expect(conditionExprFromJson.condition).toBeInstanceOf(TimeCondition);
});
- it('rpc condition serialization', async () => {
+ it('rpc condition serialization', () => {
const conditionExpr = new ConditionExpression(rpcCondition);
const conditionExprJson = conditionExpr.toJson();
diff --git a/test/unit/taco.test.ts b/test/unit/taco.test.ts
new file mode 100644
index 000000000..fe74043dd
--- /dev/null
+++ b/test/unit/taco.test.ts
@@ -0,0 +1,88 @@
+import {
+ FerveoVariant,
+ SecretKey,
+ SessionStaticSecret,
+} from '@nucypher/nucypher-core';
+
+import { conditions } from '../../src';
+import { taco } from '../../src/taco';
+import { toBytes } from '../../src/utils';
+import {
+ fakeDkgFlow,
+ fakeDkgParticipants,
+ fakeDkgRitual,
+ fakePorterUri,
+ fakeTDecFlow,
+ fakeWeb3Provider,
+ mockCbdDecrypt,
+ mockGetExistingRitual,
+ mockGetParticipants,
+ mockRandomSessionStaticSecret,
+} from '../utils';
+
+import { aliceSecretKeyBytes } from './testVariables';
+
+const {
+ predefined: { ERC721Ownership },
+ ConditionExpression,
+} = conditions;
+
+// Shared test variables
+const aliceSecretKey = SecretKey.fromBEBytes(aliceSecretKeyBytes);
+const ownsNFT = new ERC721Ownership({
+ contractAddress: '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77',
+ parameters: [3591],
+ chain: 5,
+});
+const conditionExpr = new ConditionExpression(ownsNFT);
+const variant = FerveoVariant.precomputed;
+// const ritualId = 0;
+const message = 'this is a secret';
+
+describe('taco', () => {
+ it('encrypts and decrypts', async () => {
+ const mockedDkg = fakeDkgFlow(variant, 0, 4, 4);
+ const mockedDkgRitual = fakeDkgRitual(mockedDkg);
+ const web3Provider = fakeWeb3Provider(aliceSecretKey.toBEBytes());
+ const getExistingRitualSpy = mockGetExistingRitual(mockedDkgRitual);
+
+ const tacoMk = await taco.encrypt(
+ web3Provider,
+ message,
+ conditionExpr,
+ mockedDkg.ritualId
+ );
+
+ expect(getExistingRitualSpy).toHaveBeenCalled();
+
+ const { decryptionShares } = fakeTDecFlow({
+ ...mockedDkg,
+ message: toBytes(message),
+ aad: tacoMk.aad,
+ ciphertext: tacoMk.ciphertext,
+ });
+ const { participantSecrets, participants } = fakeDkgParticipants(
+ mockedDkg.ritualId,
+ variant
+ );
+ const requesterSessionKey = SessionStaticSecret.random();
+ const decryptSpy = mockCbdDecrypt(
+ mockedDkg.ritualId,
+ decryptionShares,
+ participantSecrets,
+ requesterSessionKey.publicKey()
+ );
+ const getParticipantsSpy = mockGetParticipants(participants);
+ const sessionKeySpy = mockRandomSessionStaticSecret(requesterSessionKey);
+
+ const decryptedMessage = await taco.decrypt(
+ web3Provider,
+ tacoMk,
+ fakePorterUri
+ );
+ expect(getParticipantsSpy).toHaveBeenCalled();
+ expect(sessionKeySpy).toHaveBeenCalled();
+ expect(decryptSpy).toHaveBeenCalled();
+ expect(decryptedMessage).toEqual(toBytes(message));
+ });
+});
diff --git a/test/utils.ts b/test/utils.ts
index 69fd8186e..7f4ff2eec 100644
--- a/test/utils.ts
+++ b/test/utils.ts
@@ -304,10 +304,7 @@ export const fakeTDecFlow = ({
message,
}: FakeDkgRitualFlow) => {
// Having aggregated the transcripts, the validators can now create decryption shares
- const decryptionShares: (
- | DecryptionSharePrecomputed
- | DecryptionShareSimple
- )[] = [];
+ const decryptionShares: DecryptionShareSimple[] = [];
zip(validators, validatorKeypairs).forEach(([validator, keypair]) => {
const dkg = new Dkg(ritualId, sharesNum, threshold, validators, validator);
const aggregate = dkg.aggregateTranscript(receivedMessages);