diff --git a/abi/Coordinator.json b/abi/Coordinator.json index c572ea259..a9f0143da 100644 --- a/abi/Coordinator.json +++ b/abi/Coordinator.json @@ -3,7 +3,7 @@ "inputs": [ { "internalType": "contract IAccessControlApplication", - "name": "app", + "name": "_stakes", "type": "address" }, { @@ -12,9 +12,24 @@ "type": "uint32" }, { - "internalType": "uint32", + "internalType": "uint16", "name": "_maxDkgSize", - "type": "uint32" + "type": "uint16" + }, + { + "internalType": "address", + "name": "_admin", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "_currency", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_feeRatePerSecond", + "type": "uint256" } ], "stateMutability": "nonpayable", @@ -45,21 +60,65 @@ "name": "AggregationPosted", "type": "event" }, + { + "anonymous": false, + "inputs": [], + "name": "DefaultAdminDelayChangeCanceled", + "type": "event" + }, { "anonymous": false, "inputs": [ { - "indexed": true, - "internalType": "uint32", - "name": "ritualId", - "type": "uint32" + "indexed": false, + "internalType": "uint48", + "name": "newDelay", + "type": "uint48" }, + { + "indexed": false, + "internalType": "uint48", + "name": "effectSchedule", + "type": "uint48" + } + ], + "name": "DefaultAdminDelayChangeScheduled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "DefaultAdminTransferCanceled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ { "indexed": true, "internalType": "address", - "name": "initiator", + "name": "newAdmin", "type": "address" }, + { + "indexed": false, + "internalType": "uint48", + "name": "acceptSchedule", + "type": "uint48" + } + ], + "name": "DefaultAdminTransferScheduled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint32", + "name": "ritualId", + "type": "uint32" + }, { "indexed": false, "internalType": "bool", @@ -75,15 +134,15 @@ "inputs": [ { "indexed": false, - "internalType": "uint32", + "internalType": "uint16", "name": "oldSize", - "type": "uint32" + "type": "uint16" }, { "indexed": false, - "internalType": "uint32", + "internalType": "uint16", "name": "newSize", - "type": "uint32" + "type": "uint16" } ], "name": "MaxDkgSizeChanged", @@ -92,20 +151,118 @@ { "anonymous": false, "inputs": [ + { + "indexed": true, + "internalType": "uint32", + "name": "ritualId", + "type": "uint32" + }, + { + "indexed": true, + "internalType": "address", + "name": "participant", + "type": "address" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "word0", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "word1", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "word2", + "type": "bytes32" + } + ], + "indexed": false, + "internalType": "struct BLS12381.G2Point", + "name": "publicKey", + "type": "tuple" + } + ], + "name": "ParticipantPublicKeySet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "previousAdminRole", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "newAdminRole", + "type": "bytes32" + } + ], + "name": "RoleAdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "RoleGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, { "indexed": true, "internalType": "address", - "name": "previousOwner", + "name": "account", "type": "address" }, { "indexed": true, "internalType": "address", - "name": "newOwner", + "name": "sender", "type": "address" } ], - "name": "OwnershipTransferred", + "name": "RoleRevoked", "type": "event" }, { @@ -133,7 +290,7 @@ { "indexed": true, "internalType": "address", - "name": "initiator", + "name": "authority", "type": "address" }, { @@ -190,6 +347,52 @@ "name": "TranscriptPosted", "type": "event" }, + { + "inputs": [], + "name": "DEFAULT_ADMIN_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "INITIATOR_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "TREASURY_ROLE", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "acceptDefaultAdminTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "application", @@ -203,6 +406,39 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "beginDefaultAdminTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "cancelDefaultAdminTransfer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint48", + "name": "newDelay", + "type": "uint48" + } + ], + "name": "changeDefaultAdminDelay", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -223,11 +459,95 @@ "type": "function" }, { - "inputs": [ + "inputs": [], + "name": "currency", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "defaultAdmin", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "defaultAdminDelay", + "outputs": [ + { + "internalType": "uint48", + "name": "", + "type": "uint48" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "defaultAdminDelayIncreaseWait", + "outputs": [ + { + "internalType": "uint48", + "name": "", + "type": "uint48" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "feeRatePerSecond", + "outputs": [ { "internalType": "uint256", - "name": "ritualID", + "name": "", "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "ritualId", + "type": "uint32" + } + ], + "name": "getAuthority", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "ritualId", + "type": "uint32" }, { "internalType": "address", @@ -312,17 +632,271 @@ { "inputs": [ { - "internalType": "uint256", - "name": "ritualId", - "type": "uint256" + "internalType": "address", + "name": "_provider", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_ritualId", + "type": "uint256" + } + ], + "name": "getProviderPublicKey", + "outputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "word0", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "word1", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "word2", + "type": "bytes32" + } + ], + "internalType": "struct BLS12381.G2Point", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "ritualId", + "type": "uint32" + } + ], + "name": "getPublicKeyFromRitualId", + "outputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "word0", + "type": "bytes32" + }, + { + "internalType": "bytes16", + "name": "word1", + "type": "bytes16" + } + ], + "internalType": "struct BLS12381.G1Point", + "name": "dkgPublicKey", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "word0", + "type": "bytes32" + }, + { + "internalType": "bytes16", + "name": "word1", + "type": "bytes16" + } + ], + "internalType": "struct BLS12381.G1Point", + "name": "dkgPublicKey", + "type": "tuple" + } + ], + "name": "getRitualIdFromPublicKey", + "outputs": [ + { + "internalType": "uint32", + "name": "ritualId", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "providers", + "type": "address[]" + }, + { + "internalType": "uint32", + "name": "duration", + "type": "uint32" + } + ], + "name": "getRitualInitiationCost", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "ritualId", + "type": "uint32" + } + ], + "name": "getRitualState", + "outputs": [ + { + "internalType": "enum Coordinator.RitualState", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + } + ], + "name": "getRoleAdmin", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "size", + "type": "uint16" + } + ], + "name": "getThresholdForRitualSize", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "grantRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "hasRole", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "providers", + "type": "address[]" + }, + { + "internalType": "address", + "name": "authority", + "type": "address" + }, + { + "internalType": "uint32", + "name": "duration", + "type": "uint32" + }, + { + "internalType": "contract IEncryptionAuthorizer", + "name": "accessController", + "type": "address" } ], - "name": "getRitualState", + "name": "initiateRitual", "outputs": [ { - "internalType": "enum Coordinator.RitualState", + "internalType": "uint32", "name": "", - "type": "uint8" + "type": "uint32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "isInitiationPublic", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" } ], "stateMutability": "view", @@ -331,19 +905,26 @@ { "inputs": [ { - "internalType": "address[]", - "name": "providers", - "type": "address[]" + "internalType": "uint32", + "name": "ritualId", + "type": "uint32" } ], - "name": "initiateRitual", + "name": "isRitualFinalized", "outputs": [ { - "internalType": "uint32", + "internalType": "bool", "name": "", - "type": "uint32" + "type": "bool" } ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "makeInitiationPublic", + "outputs": [], "stateMutability": "nonpayable", "type": "function" }, @@ -352,9 +933,9 @@ "name": "maxDkgSize", "outputs": [ { - "internalType": "uint32", + "internalType": "uint16", "name": "", - "type": "uint32" + "type": "uint16" } ], "stateMutability": "view", @@ -386,6 +967,61 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "pendingDefaultAdmin", + "outputs": [ + { + "internalType": "address", + "name": "newAdmin", + "type": "address" + }, + { + "internalType": "uint48", + "name": "schedule", + "type": "uint48" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingDefaultAdminDelay", + "outputs": [ + { + "internalType": "uint48", + "name": "newDelay", + "type": "uint48" + }, + { + "internalType": "uint48", + "name": "schedule", + "type": "uint48" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "pendingFees", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -412,7 +1048,7 @@ } ], "internalType": "struct BLS12381.G1Point", - "name": "publicKey", + "name": "dkgPublicKey", "type": "tuple" }, { @@ -445,8 +1081,50 @@ "type": "function" }, { - "inputs": [], - "name": "renounceOwnership", + "inputs": [ + { + "internalType": "uint32", + "name": "ritualId", + "type": "uint32" + } + ], + "name": "processPendingFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "renounceRole", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "role", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "revokeRole", "outputs": [], "stateMutability": "nonpayable", "type": "function" @@ -468,23 +1146,48 @@ }, { "internalType": "uint32", - "name": "dkgSize", + "name": "initTimestamp", "type": "uint32" }, { "internalType": "uint32", - "name": "initTimestamp", + "name": "endTimestamp", "type": "uint32" }, { - "internalType": "uint32", + "internalType": "uint16", "name": "totalTranscripts", - "type": "uint32" + "type": "uint16" }, { - "internalType": "uint32", + "internalType": "uint16", "name": "totalAggregations", - "type": "uint32" + "type": "uint16" + }, + { + "internalType": "address", + "name": "authority", + "type": "address" + }, + { + "internalType": "uint16", + "name": "dkgSize", + "type": "uint16" + }, + { + "internalType": "uint16", + "name": "threshold", + "type": "uint16" + }, + { + "internalType": "bool", + "name": "aggregationMismatch", + "type": "bool" + }, + { + "internalType": "contract IEncryptionAuthorizer", + "name": "accessController", + "type": "address" }, { "components": [ @@ -503,11 +1206,6 @@ "name": "publicKey", "type": "tuple" }, - { - "internalType": "bool", - "name": "aggregationMismatch", - "type": "bool" - }, { "internalType": "bytes", "name": "aggregatedTranscript", @@ -517,12 +1215,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "rollbackDefaultAdminDelay", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { - "internalType": "uint32", + "internalType": "uint16", "name": "newSize", - "type": "uint32" + "type": "uint16" } ], "name": "setMaxDkgSize", @@ -530,6 +1235,67 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "word0", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "word1", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "word2", + "type": "bytes32" + } + ], + "internalType": "struct BLS12381.G2Point", + "name": "_publicKey", + "type": "tuple" + } + ], + "name": "setProviderPublicKey", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IReimbursementPool", + "name": "pool", + "type": "address" + } + ], + "name": "setReimbursementPool", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "ritualId", + "type": "uint32" + }, + { + "internalType": "address", + "name": "authority", + "type": "address" + } + ], + "name": "setRitualAuthority", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -543,6 +1309,25 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "timeout", @@ -556,15 +1341,33 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "totalPendingFees", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { - "internalType": "address", - "name": "newOwner", + "internalType": "contract IERC20", + "name": "token", "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" } ], - "name": "transferOwnership", + "name": "withdrawTokens", "outputs": [], "stateMutability": "nonpayable", "type": "function" diff --git a/src/agents/coordinator.ts b/src/agents/coordinator.ts index db8378d8d..3f480e698 100644 --- a/src/agents/coordinator.ts +++ b/src/agents/coordinator.ts @@ -1,5 +1,5 @@ -import { SessionStaticKey } from '@nucypher/nucypher-core'; -import { ethers } from 'ethers'; +import { DkgPublicKey, SessionStaticKey } from '@nucypher/nucypher-core'; +import { BigNumberish, ethers } from 'ethers'; import { Coordinator, @@ -13,12 +13,16 @@ import { DEFAULT_WAIT_N_CONFIRMATIONS, getContract } from './contracts'; export interface CoordinatorRitual { initiator: string; - dkgSize: number; initTimestamp: number; + endTimestamp: number; totalTranscripts: number; totalAggregations: number; - publicKey: BLS12381.G1PointStructOutput; + authority: string; + dkgSize: number; + threshold: number; aggregationMismatch: boolean; + accessController: string; + publicKey: BLS12381.G1PointStructOutput; aggregatedTranscript: string; } @@ -59,10 +63,18 @@ export class DkgCoordinatorAgent { public static async initializeRitual( provider: ethers.providers.Provider, signer: ethers.Signer, - providers: ChecksumAddress[] + providers: ChecksumAddress[], + authority: string, + duration: BigNumberish, + accessController: string ): Promise { const Coordinator = await this.connectReadWrite(provider, signer); - const tx = await Coordinator.initiateRitual(providers); + const tx = await Coordinator.initiateRitual( + providers, + authority, + duration, + accessController + ); const txReceipt = await tx.wait(DEFAULT_WAIT_N_CONFIRMATIONS); const [ritualStartEvent] = txReceipt.events ?? []; if (!ritualStartEvent) { @@ -95,16 +107,25 @@ export class DkgCoordinatorAgent { const Coordinator = await this.connectReadOnly(provider); // We leave `initiator` undefined because we don't care who the initiator is // We leave `successful` undefined because we don't care if the ritual was successful - const eventFilter = Coordinator.filters.EndRitual( - ritualId, - undefined, - undefined - ); - Coordinator.once(eventFilter, (_ritualId, _initiator, successful) => { + const eventFilter = Coordinator.filters.EndRitual(ritualId, undefined); + Coordinator.once(eventFilter, (_ritualId, successful) => { callback(successful); }); } + public static async getRitualIdFromPublicKey( + provider: ethers.providers.Provider, + dkgPublicKey: DkgPublicKey + ): Promise { + const Coordinator = await this.connectReadOnly(provider); + const dkgPublicKeyBytes = dkgPublicKey.toBytes(); + const pointStruct: BLS12381.G1PointStruct = { + word0: dkgPublicKeyBytes.slice(0, 32), + word1: dkgPublicKeyBytes.slice(32, 48), + }; + return await Coordinator.getRitualIdFromPublicKey(pointStruct); + } + private static async connectReadOnly(provider: ethers.providers.Provider) { return await this.connect(provider); } diff --git a/src/characters/cbd-recipient.ts b/src/characters/cbd-recipient.ts index ad973616a..585ad7faa 100644 --- a/src/characters/cbd-recipient.ts +++ b/src/characters/cbd-recipient.ts @@ -14,7 +14,6 @@ import { ethers } from 'ethers'; import { DkgCoordinatorAgent, DkgParticipant } from '../agents/coordinator'; import { ConditionContext } from '../conditions'; -import { DkgRitual } from '../dkg'; import { PorterClient } from '../porter'; import { fromJSON, objectEquals, toJSON } from '../utils'; @@ -31,22 +30,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.Provider, + web3Provider: ethers.providers.Provider, thresholdMessageKit: ThresholdMessageKit, signer?: ethers.Signer ): Promise { const decryptionShares = await this.retrieve( - provider, + web3Provider, thresholdMessageKit, signer ); diff --git a/src/conditions/context/context.ts b/src/conditions/context/context.ts index c1d8426df..feeeb257c 100644 --- a/src/conditions/context/context.ts +++ b/src/conditions/context/context.ts @@ -51,7 +51,7 @@ export class ConditionContext { ); if (conditionRequiresSigner && !this.signer) { throw new Error( - `Condition contains ${USER_ADDRESS_PARAM} context variable and requires a signer to populate` + `Signer required to satisfy ${USER_ADDRESS_PARAM} context variable in condition` ); } diff --git a/src/dkg.ts b/src/dkg.ts index c1ab06499..479213f3e 100644 --- a/src/dkg.ts +++ b/src/dkg.ts @@ -1,19 +1,15 @@ import { DkgPublicKey } from '@nucypher/nucypher-core'; -import { ethers } from 'ethers'; +import { BigNumberish, ethers } from 'ethers'; import { DkgCoordinatorAgent, DkgRitualState } from './agents/coordinator'; import { ChecksumAddress } from './types'; -import { fromHexString, objectEquals } from './utils'; - -export type DkgRitualParameters = { - sharesNum: number; - threshold: number; -}; +import { fromHexString } from './utils'; export interface DkgRitualJSON { id: number; dkgPublicKey: Uint8Array; - dkgParams: DkgRitualParameters; + sharesNum: number; + threshold: number; state: DkgRitualState; } @@ -21,7 +17,8 @@ export class DkgRitual { constructor( public readonly id: number, public readonly dkgPublicKey: DkgPublicKey, - public readonly dkgParams: DkgRitualParameters, + public readonly sharesNum: number, + public readonly threshold: number, public readonly state: DkgRitualState ) {} @@ -29,7 +26,8 @@ export class DkgRitual { return { id: this.id, dkgPublicKey: this.dkgPublicKey.toBytes(), - dkgParams: this.dkgParams, + sharesNum: this.sharesNum, + threshold: this.threshold, state: this.state, }; } @@ -37,13 +35,15 @@ export class DkgRitual { public static fromObj({ id, dkgPublicKey, - dkgParams, + sharesNum, + threshold, state, }: DkgRitualJSON): DkgRitual { return new DkgRitual( id, DkgPublicKey.fromBytes(dkgPublicKey), - dkgParams, + sharesNum, + threshold, state ); } @@ -52,28 +52,30 @@ export class DkgRitual { return [ this.id === other.id, this.dkgPublicKey.equals(other.dkgPublicKey), - objectEquals(this.dkgParams, other.dkgParams), + this.sharesNum === other.sharesNum, + this.threshold === other.threshold, this.state === other.state, ].every(Boolean); } } -// TODO: Currently, we're assuming that the threshold is always `floor(sharesNum / 2) + 1`. -// https://github.com/nucypher/nucypher/issues/3095 -const assumedThreshold = (sharesNum: number): number => - Math.floor(sharesNum / 2) + 1; - export class DkgClient { public static async initializeRitual( provider: ethers.providers.Provider, signer: ethers.Signer, ursulas: ChecksumAddress[], + authority: string, + duration: BigNumberish, + accessController: string, waitUntilEnd = false ): Promise { const ritualId = await DkgCoordinatorAgent.initializeRitual( provider, signer, - ursulas.sort() + ursulas.sort(), + authority, + duration, + accessController ); if (waitUntilEnd) { @@ -111,7 +113,7 @@ export class DkgClient { }); }; - public static async getExistingRitual( + public static async getRitual( provider: ethers.providers.Provider, ritualId: number ): Promise { @@ -127,14 +129,25 @@ export class DkgClient { return new DkgRitual( ritualId, DkgPublicKey.fromBytes(dkgPkBytes), - { - sharesNum: ritual.dkgSize, - threshold: assumedThreshold(ritual.dkgSize), - }, + ritual.dkgSize, + ritual.threshold, ritualState ); } + public static async getFinalizedRitual( + provider: ethers.providers.Provider, + ritualId: number + ): Promise { + const ritual = await DkgClient.getRitual(provider, 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 56a91c6c3..acca9c3f7 100644 --- a/src/sdk/strategy/cbd-strategy.ts +++ b/src/sdk/strategy/cbd-strategy.ts @@ -44,7 +44,7 @@ export class CbdStrategy { // // Given that we just initialized the ritual, this should never happen // throw new Error('Ritual ID is undefined'); // } - const dkgRitual = await DkgClient.getExistingRitual(provider, ritualId); + const dkgRitual = await DkgClient.getRitual(provider, ritualId); return DeployedCbdStrategy.create(dkgRitual, this.cohort.porterUri); } @@ -78,7 +78,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.threshold + ); return new DeployedCbdStrategy(decrypter, dkgRitual.dkgPublicKey); } @@ -88,7 +92,7 @@ export class DeployedCbdStrategy { porterUri: string, ritualId: number ): Promise { - const dkgRitual = await DkgClient.getExistingRitual(provider, ritualId); + const dkgRitual = await DkgClient.getRitual(provider, ritualId); return DeployedCbdStrategy.create(dkgRitual, porterUri); } diff --git a/src/taco.ts b/src/taco.ts new file mode 100644 index 000000000..13034bef7 --- /dev/null +++ b/src/taco.ts @@ -0,0 +1,55 @@ +import { DkgPublicKey, ThresholdMessageKit } from '@nucypher/nucypher-core'; +import { ethers } from 'ethers'; + +import { DkgCoordinatorAgent } from './agents/coordinator'; +import { ThresholdDecrypter } from './characters/cbd-recipient'; +import { Enrico } from './characters/enrico'; +import { Condition, ConditionExpression } from './conditions'; +import { DkgClient } from './dkg'; +import { getPorterUri } from './porter'; +import { toBytes } from './utils'; + +export const encrypt = async ( + provider: ethers.providers.Provider, + message: string, + condition: Condition, + ritualId: number +): Promise => { + const dkgRitual = await DkgClient.getFinalizedRitual(provider, ritualId); + return await encryptLight(message, condition, dkgRitual.dkgPublicKey); +}; + +export const encryptLight = async ( + message: string, + condition: Condition, + dkgPublicKey: DkgPublicKey +): Promise => { + const encrypter = new Enrico(dkgPublicKey); + const conditionExpr = new ConditionExpression(condition); + return encrypter.encryptMessageCbd(toBytes(message), conditionExpr); +}; + +export const decrypt = async ( + provider: ethers.providers.Provider, + messageKit: ThresholdMessageKit, + signer?: ethers.Signer, + porterUri = getPorterUri('tapir') +): Promise => { + const ritualId = await DkgCoordinatorAgent.getRitualIdFromPublicKey( + provider, + messageKit.acp.publicKey + ); + const ritual = await DkgClient.getFinalizedRitual(provider, ritualId); + const decrypter = ThresholdDecrypter.create( + porterUri, + ritualId, + ritual.threshold + ); + return decrypter.retrieveAndDecrypt(provider, messageKit, signer); +}; + +export const taco = { + encrypt, + encryptLight, + decrypt, +}; diff --git a/test/unit/cbd-strategy.test.ts b/test/unit/cbd-strategy.test.ts index 7251262b1..51810e531 100644 --- a/test/unit/cbd-strategy.test.ts +++ b/test/unit/cbd-strategy.test.ts @@ -15,8 +15,8 @@ import { fakeUrsulas, makeCohort, mockCbdDecrypt, - mockGetExistingRitual, mockGetParticipants, + mockGetRitual, mockGetUrsulas, mockRandomSessionStaticSecret, } from '../utils'; @@ -29,9 +29,9 @@ const { } = conditions; // Shared test variables -const aliceSecretKey = SecretKey.fromBEBytes(aliceSecretKeyBytes); -const aliceProvider = fakeProvider(aliceSecretKey.toBEBytes()); -const aliceSigner = fakeSigner(aliceSecretKey.toBEBytes()); +const secretKey = SecretKey.fromBEBytes(aliceSecretKeyBytes); +const provider = fakeProvider(secretKey.toBEBytes()); +const signer = fakeSigner(secretKey.toBEBytes()); const ownsNFT = new ERC721Ownership({ contractAddress: '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77', parameters: [3591], @@ -51,12 +51,12 @@ const makeCbdStrategy = async () => { async function makeDeployedCbdStrategy() { const strategy = await makeCbdStrategy(); - const mockedDkg = fakeDkgFlow(variant, 0, 4, 4); const mockedDkgRitual = fakeDkgRitual(mockedDkg); const getUrsulasSpy = mockGetUrsulas(ursulas); - const getExistingRitualSpy = mockGetExistingRitual(mockedDkgRitual); - const deployedStrategy = await strategy.deploy(aliceProvider, ritualId); + const getExistingRitualSpy = mockGetRitual(mockedDkgRitual); + + const deployedStrategy = await strategy.deploy(provider, ritualId); expect(getUrsulasSpy).toHaveBeenCalled(); expect(getExistingRitualSpy).toHaveBeenCalled(); @@ -108,13 +108,13 @@ describe('CbdDeployedStrategy', () => { .encryptMessageCbd(message); // Setup mocks for `retrieveAndDecrypt` - const { decryptionShares } = await fakeTDecFlow({ + const { decryptionShares } = fakeTDecFlow({ ...mockedDkg, message: toBytes(message), dkgPublicKey: mockedDkg.dkg.publicKey(), thresholdMessageKit, }); - const { participantSecrets, participants } = await fakeDkgParticipants( + const { participantSecrets, participants } = fakeDkgParticipants( mockedDkg.ritualId ); const requesterSessionKey = SessionStaticSecret.random(); @@ -130,9 +130,9 @@ describe('CbdDeployedStrategy', () => { const decryptedMessage = await deployedStrategy.decrypter.retrieveAndDecrypt( - aliceProvider, + provider, thresholdMessageKit, - aliceSigner + signer ); expect(getUrsulasSpy).toHaveBeenCalled(); expect(getParticipantsSpy).toHaveBeenCalled(); diff --git a/test/unit/conditions/condition-expr.test.ts b/test/unit/conditions/condition-expr.test.ts index 1ef17774a..6941f834d 100644 --- a/test/unit/conditions/condition-expr.test.ts +++ b/test/unit/conditions/condition-expr.test.ts @@ -9,7 +9,7 @@ import { TimeCondition, TimeConditionProps, } from '../../../src/conditions/base'; -import { RpcConditionType } from '../../../src/conditions/base/rpc'; +import { RpcConditionType } from '../../../src/conditions/base'; import { USER_ADDRESS_PARAM } from '../../../src/conditions/const'; import { ERC721Balance } from '../../../src/conditions/predefined'; import { objectEquals, toJSON } from '../../../src/utils'; @@ -187,6 +187,12 @@ describe('condition set', () => { ConditionExpression.fromJSON(conditionExprJson); expect(conditionExprFromJson).toBeDefined(); expect(conditionExprFromJson.equals(conditionExprFromJson)).toBeTruthy(); + + const asWasmConditions = conditionExprFromJson.toWASMConditions(); + const fromWasmConditions = + ConditionExpression.fromWASMConditions(asWasmConditions); + expect(fromWasmConditions).toBeDefined(); + expect(fromWasmConditions.equals(conditionExprFromJson)).toBeTruthy(); }); it('serializes to and from WASM conditions', () => { diff --git a/test/unit/conditions/context.test.ts b/test/unit/conditions/context.test.ts index 4a0815912..dc629c7a5 100644 --- a/test/unit/conditions/context.test.ts +++ b/test/unit/conditions/context.test.ts @@ -91,6 +91,10 @@ describe('context parameters', () => { const condition = new ContractCondition(conditionObj); const conditionExpr = new ConditionExpression(condition); expect(conditionExpr.contextRequiresSigner()).toBe(true); + expect(conditionExpr.buildContext(provider, {}, signer)).toBeDefined(); + expect(() => conditionExpr.buildContext(provider, {})).toThrow( + `Signer required to satisfy ${USER_ADDRESS_PARAM} context variable in condition` + ); }); it('detects if a signer is not required', () => { @@ -100,6 +104,8 @@ describe('context parameters', () => { false ); expect(conditionExpr.contextRequiresSigner()).toBe(false); + expect(conditionExpr.buildContext(provider, {}, signer)).toBeDefined(); + expect(conditionExpr.buildContext(provider, {})).toBeDefined(); }); it('rejects on a missing signer', () => { @@ -114,7 +120,7 @@ describe('context parameters', () => { const conditionExpr = new ConditionExpression(condition); expect(conditionExpr.contextRequiresSigner()).toBe(true); expect(() => conditionExpr.buildContext(provider, {}, undefined)).toThrow( - `Condition contains ${USER_ADDRESS_PARAM} context variable and requires a signer to populate` + `Signer required to satisfy ${USER_ADDRESS_PARAM} context variable in condition` ); }); diff --git a/test/unit/taco.test.ts b/test/unit/taco.test.ts new file mode 100644 index 000000000..2648abff9 --- /dev/null +++ b/test/unit/taco.test.ts @@ -0,0 +1,94 @@ +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, + fakeProvider, + fakeSigner, + fakeTDecFlow, + mockCbdDecrypt, + mockGetParticipants, + mockGetRitual, + mockGetRitualIdFromPublicKey, + mockRandomSessionStaticSecret, +} from '../utils'; + +import { aliceSecretKeyBytes } from './testVariables'; + +const { + predefined: { ERC721Ownership }, +} = conditions; + +// Shared test variables +const aliceSecretKey = SecretKey.fromBEBytes(aliceSecretKeyBytes); +const ownsNFT = new ERC721Ownership({ + contractAddress: '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77', + parameters: [3591], + chain: 5, +}); +const variant = FerveoVariant.precomputed; +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 provider = fakeProvider(aliceSecretKey.toBEBytes()); + const signer = fakeSigner(aliceSecretKey.toBEBytes()); + const getExistingRitualSpy = mockGetRitual(mockedDkgRitual); + + const messageKit = await taco.encrypt( + provider, + message, + ownsNFT, + mockedDkg.ritualId + ); + + expect(getExistingRitualSpy).toHaveBeenCalled(); + + const { decryptionShares } = fakeTDecFlow({ + ...mockedDkg, + message: toBytes(message), + dkgPublicKey: mockedDkg.dkg.publicKey(), + thresholdMessageKit: messageKit, + }); + const { participantSecrets, participants } = fakeDkgParticipants( + mockedDkg.ritualId + ); + const requesterSessionKey = SessionStaticSecret.random(); + const decryptSpy = mockCbdDecrypt( + mockedDkg.ritualId, + decryptionShares, + participantSecrets, + requesterSessionKey.publicKey() + ); + const getParticipantsSpy = mockGetParticipants(participants); + const sessionKeySpy = mockRandomSessionStaticSecret(requesterSessionKey); + const getRitualIdFromPublicKey = mockGetRitualIdFromPublicKey( + mockedDkg.ritualId + ); + const getRitualSpy = mockGetRitual(mockedDkgRitual); + + const decryptedMessage = await taco.decrypt( + provider, + messageKit, + signer, + fakePorterUri + ); + expect(getParticipantsSpy).toHaveBeenCalled(); + expect(sessionKeySpy).toHaveBeenCalled(); + expect(getRitualIdFromPublicKey).toHaveBeenCalled(); + expect(getRitualSpy).toHaveBeenCalled(); + expect(decryptSpy).toHaveBeenCalled(); + expect(decryptedMessage).toEqual(toBytes(message)); + }); +}); diff --git a/test/utils.ts b/test/utils.ts index 11f8b6714..ec8b1aeee 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -313,7 +313,7 @@ interface FakeDkgRitualFlow { thresholdMessageKit: ThresholdMessageKit; } -export const fakeTDecFlow = async ({ +export const fakeTDecFlow = ({ validators, validatorKeypairs, ritualId, @@ -324,10 +324,7 @@ export const fakeTDecFlow = async ({ thresholdMessageKit, }: 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); @@ -367,7 +364,7 @@ const fakeConditionExpr = () => { return new ConditionExpression(erc721Balance); }; -export const fakeDkgTDecFlowE2E = async ( +export const fakeDkgTDecFlowE2E = ( ritualId = 0, variant: FerveoVariant = FerveoVariant.precomputed, conditionExpr: ConditionExpression = fakeConditionExpr(), @@ -382,7 +379,7 @@ export const fakeDkgTDecFlowE2E = async ( conditionExpr ); - const { decryptionShares } = await fakeTDecFlow({ + const { decryptionShares } = fakeTDecFlow({ ...ritual, message, dkgPublicKey, @@ -432,13 +429,13 @@ export const fakeCoordinatorRitual = async ( }; }; -export const fakeDkgParticipants = async ( +export const fakeDkgParticipants = ( ritualId: number -): Promise<{ +): { participants: DkgParticipant[]; participantSecrets: Record; -}> => { - const ritual = await fakeDkgTDecFlowE2E(ritualId); +} => { + const ritual = fakeDkgTDecFlowE2E(ritualId); const label = toBytes(`${ritualId}`); const participantSecrets: Record = @@ -519,20 +516,26 @@ export const fakeDkgRitual = (ritual: { return new DkgRitual( fakeRitualId, ritual.dkg.publicKey(), - { - sharesNum: ritual.sharesNum, - threshold: ritual.threshold, - }, + ritual.sharesNum, + ritual.threshold, DkgRitualState.FINALIZED ); }; -export const mockGetExistingRitual = (dkgRitual: DkgRitual) => { - return jest.spyOn(DkgClient, 'getExistingRitual').mockImplementation(() => { +export const mockGetRitual = (dkgRitual: DkgRitual) => { + return jest.spyOn(DkgClient, 'getRitual').mockImplementation(() => { return Promise.resolve(dkgRitual); }); }; +export const mockGetRitualIdFromPublicKey = (ritualId: number) => { + return jest + .spyOn(DkgCoordinatorAgent, 'getRitualIdFromPublicKey') + .mockImplementation(() => { + return Promise.resolve(ritualId); + }); +}; + export const makeCohort = async (ursulas: Ursula[]) => { const getUrsulasSpy = mockGetUrsulas(ursulas); const porterUri = 'https://_this.should.crash';