Skip to content
This repository has been archived by the owner on Aug 1, 2024. It is now read-only.

Commit

Permalink
Add ritual initialization to client (nucypher#233)
Browse files Browse the repository at this point in the history
  • Loading branch information
piotr-roslaniec committed Aug 7, 2023
2 parents a1a1a65 + 24b97d1 commit 1ad1ad9
Show file tree
Hide file tree
Showing 14 changed files with 321 additions and 94 deletions.
72 changes: 66 additions & 6 deletions src/agents/coordinator.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { SessionStaticKey } from '@nucypher/nucypher-core';
import { SessionStaticKey, Transcript } from '@nucypher/nucypher-core';
import { ethers } from 'ethers';

import {
Coordinator,
Coordinator__factory,
} from '../../types/ethers-contracts';
import { BLS12381 } from '../../types/ethers-contracts/Coordinator';
import { ChecksumAddress } from '../types';
import { fromHexString } from '../utils';

import { getContract } from './contracts';
import { DEFAULT_WAIT_N_CONFIRMATIONS, getContract } from './contracts';

export interface CoordinatorRitual {
initiator: string;
Expand All @@ -24,12 +25,22 @@ export interface CoordinatorRitual {
export type DkgParticipant = {
provider: string;
aggregated: boolean;
transcript: Transcript;
decryptionRequestStaticKey: SessionStaticKey;
};

export enum DkgRitualState {
NON_INITIATED,
AWAITING_TRANSCRIPTS,
AWAITING_AGGREGATIONS,
TIMEOUT,
INVALID,
FINALIZED,
}

export class DkgCoordinatorAgent {
public static async getParticipants(
provider: ethers.providers.Provider,
provider: ethers.providers.Web3Provider,
ritualId: number
): Promise<DkgParticipant[]> {
const Coordinator = await this.connectReadOnly(provider);
Expand All @@ -39,27 +50,76 @@ export class DkgCoordinatorAgent {
return {
provider: participant.provider,
aggregated: participant.aggregated,
transcript: Transcript.fromBytes(fromHexString(participant.transcript)),
decryptionRequestStaticKey: SessionStaticKey.fromBytes(
fromHexString(participant.decryptionRequestStaticKey)
),
};
});
}

public static async initializeRitual(
provider: ethers.providers.Web3Provider,
providers: ChecksumAddress[]
): Promise<number> {
const Coordinator = await this.connectReadWrite(provider);
const tx = await Coordinator.initiateRitual(providers);
const txReceipt = await tx.wait(DEFAULT_WAIT_N_CONFIRMATIONS);
const [ritualStartEvent] = txReceipt.events ?? [];
if (!ritualStartEvent) {
throw new Error('Ritual start event not found');
}
return ritualStartEvent.args?.ritualId;
}

public static async getRitual(
provider: ethers.providers.Provider,
provider: ethers.providers.Web3Provider,
ritualId: number
): Promise<CoordinatorRitual> {
const Coordinator = await this.connectReadOnly(provider);
return Coordinator.rituals(ritualId);
}

private static async connectReadOnly(provider: ethers.providers.Provider) {
public static async getRitualState(
provider: ethers.providers.Web3Provider,
ritualId: number
): Promise<DkgRitualState> {
const Coordinator = await this.connectReadOnly(provider);
return await Coordinator.getRitualState(ritualId);
}

public static async onRitualEndEvent(
provider: ethers.providers.Web3Provider,
ritualId: number,
callback: (successful: boolean) => void
): Promise<void> {
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) => {
callback(successful);
});
}

private static async connectReadOnly(
provider: ethers.providers.Web3Provider
) {
return await this.connect(provider);
}

private static async connectReadWrite(
web3Provider: ethers.providers.Web3Provider
) {
return await this.connect(web3Provider, web3Provider.getSigner());
}

private static async connect(
provider: ethers.providers.Provider,
provider: ethers.providers.Web3Provider,
signer?: ethers.providers.JsonRpcSigner
): Promise<Coordinator> {
const network = await provider.getNetwork();
Expand Down
2 changes: 1 addition & 1 deletion src/characters/cbd-recipient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class CbdTDecDecrypter {
return new CbdTDecDecrypter(
new Porter(porterUri),
dkgRitual.id,
dkgRitual.threshold
dkgRitual.dkgParams.threshold
);
}

Expand Down
183 changes: 141 additions & 42 deletions src/dkg.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import {
AggregatedTranscript,
combineDecryptionSharesPrecomputed,
combineDecryptionSharesSimple,
DecryptionSharePrecomputed,
DecryptionShareSimple,
DkgPublicKey,
EthereumAddress,
FerveoPublicKey,
SharedSecret,
Validator,
ValidatorMessage,
} from '@nucypher/nucypher-core';
import { ethers } from 'ethers';

import { DkgCoordinatorAgent } from './agents/coordinator';
import { bytesEquals, fromHexString } from './utils';
import { DkgCoordinatorAgent, DkgRitualState } from './agents/coordinator';
import { ChecksumAddress } from './types';
import { bytesEquals, fromHexString, objectEquals } from './utils';

// TODO: Expose from @nucypher/nucypher-core
export enum FerveoVariant {
Expand Down Expand Up @@ -45,85 +51,178 @@ export function getCombineDecryptionSharesFunction(
}
}

export type DkgRitualParameters = {
sharesNum: number;
threshold: number;
};

export interface DkgRitualJSON {
id: number;
dkgPublicKey: Uint8Array;
threshold: number;
dkgParams: DkgRitualParameters;
state: DkgRitualState;
}

export class DkgRitual {
constructor(
public readonly id: number,
public readonly dkgPublicKey: DkgPublicKey,
public readonly threshold: number
public readonly dkgParams: DkgRitualParameters,
public readonly state: DkgRitualState
) {}

public toObj(): DkgRitualJSON {
return {
id: this.id,
dkgPublicKey: this.dkgPublicKey.toBytes(),
threshold: this.threshold,
dkgParams: this.dkgParams,
state: this.state,
};
}

public static fromObj({
id,
dkgPublicKey,
threshold,
dkgParams,
state,
}: DkgRitualJSON): DkgRitual {
return new DkgRitual(id, DkgPublicKey.fromBytes(dkgPublicKey), threshold);
return new DkgRitual(
id,
DkgPublicKey.fromBytes(dkgPublicKey),
dkgParams,
state
);
}

public equals(other: DkgRitual): boolean {
return (
this.id === other.id &&
// TODO: Replace with `equals` after https://github.com/nucypher/nucypher-core/issues/56 is fixed
bytesEquals(this.dkgPublicKey.toBytes(), other.dkgPublicKey.toBytes()) &&
this.threshold === other.threshold
objectEquals(this.dkgParams, other.dkgParams) &&
this.state === other.state
);
}
}

// 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 {
constructor(private readonly provider: ethers.providers.Web3Provider) {}

// TODO: Update API: Replace with getExistingRitual and support ritualId in Strategy
public async initializeRitual(ritualParams: {
shares: number;
threshold: number;
}): Promise<DkgRitual> {
const ritualId = 2;
const ritual = await DkgCoordinatorAgent.getRitual(this.provider, ritualId);
public static async initializeRitual(
web3Provider: ethers.providers.Web3Provider,
ursulas: ChecksumAddress[],
waitUntilEnd = false
): Promise<number | undefined> {
const ritualId = await DkgCoordinatorAgent.initializeRitual(
web3Provider,
ursulas.sort()
);

if (waitUntilEnd) {
const isSuccessful = await DkgClient.waitUntilRitualEnd(
web3Provider,
ritualId
);
if (!isSuccessful) {
const ritualState = await DkgCoordinatorAgent.getRitualState(
web3Provider,
ritualId
);
throw new Error(
`Ritual initialization failed. Ritual id ${ritualId} is in state ${ritualState}`
);
}
}

return ritualId;
}

private static waitUntilRitualEnd = async (
web3Provider: ethers.providers.Web3Provider,
ritualId: number
): Promise<boolean> => {
return new Promise((resolve, reject) => {
const callback = (successful: boolean) => {
if (successful) {
resolve(true);
} else {
reject();
}
};
DkgCoordinatorAgent.onRitualEndEvent(web3Provider, ritualId, callback);
});
};

public static async getExistingRitual(
web3Provider: ethers.providers.Web3Provider,
ritualId: number
): Promise<DkgRitual> {
const ritualState = await DkgCoordinatorAgent.getRitualState(
web3Provider,
ritualId
);
const ritual = await DkgCoordinatorAgent.getRitual(web3Provider, ritualId);
const dkgPkBytes = new Uint8Array([
...fromHexString(ritual.publicKey.word0),
...fromHexString(ritual.publicKey.word1),
]);
return new DkgRitual(
ritualId,
DkgPublicKey.fromBytes(dkgPkBytes),
{
sharesNum: ritual.dkgSize,
threshold: assumedThreshold(ritual.dkgSize),
},
ritualState
);
}

return {
id: ritualId,
dkgPublicKey: DkgPublicKey.fromBytes(dkgPkBytes),
threshold: ritualParams.threshold,
} as DkgRitual;
public static async verifyRitual(
web3Provider: ethers.providers.Web3Provider,
ritualId: number
): Promise<boolean> {
const ritual = await DkgCoordinatorAgent.getRitual(web3Provider, ritualId);
const participants = await DkgCoordinatorAgent.getParticipants(
web3Provider,
ritualId
);

const validatorMessages = participants.map((p) => {
const validatorAddress = EthereumAddress.fromString(p.provider);
// TODO: Replace with real keys
// const publicKey = FerveoPublicKey.fromBytes(fromHexString(p.???));
const publicKey = DkgClient.getParticipantPublicKey(p.provider);
const validator = new Validator(validatorAddress, publicKey);
return new ValidatorMessage(validator, p.transcript);
});
const aggregate = new AggregatedTranscript(validatorMessages);

return aggregate.verify(ritual.dkgSize, validatorMessages);
}

// 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<boolean> {
// const ritual = await DkgCoordinatorAgent.getRitual(this.provider, ritualId);
// const participants = await DkgCoordinatorAgent.getParticipants(
// this.provider,
// ritualId
// );
//
// const validatorMessages = participants.map((p) => {
// const validatorAddress = EthereumAddress.fromString(p.provider);
// const publicKey = FerveoPublicKey.fromBytes(fromHexString(p.???));
// const validator = new Validator(validatorAddress, publicKey);
// const transcript = Transcript.fromBytes(fromHexString(p.transcript));
// return new ValidatorMessage(validator, transcript);
// });
// const aggregate = new AggregatedTranscript(validatorMessages);
//
// return aggregate.verify(ritual.dkgSize, validatorMessages);
// }
public static getParticipantPublicKey = (address: string) => {
// TODO: Without Validator public key in Coordinator, we cannot verify the
// transcript. We need to add it to the Coordinator (nucypher-contracts #77).
const participantPublicKeys: Record<string, FerveoPublicKey> = {
'0x210eeAC07542F815ebB6FD6689637D8cA2689392': FerveoPublicKey.fromBytes(
fromHexString(
'6000000000000000ace9d7567b26dafc512b2303cfdaa872850c62b100078ddeaabf8408c7308b3a43dfeb88375c21ef63230fb4008ce7e908764463c6765e556f9b03009eb1757d179eaa26bf875332807cc070d62a385ed2e66e09f4f4766451da12779a09036e'
)
),
'0xb15d5A4e2be34f4bE154A1b08a94Ab920FfD8A41': FerveoPublicKey.fromBytes(
fromHexString(
'60000000000000008b373fdb6b43e9dca028bd603c2bf90f0e008ec83ff217a8d7bc006b585570e6ab1ce761bad0e21c1aed1363286145f61134ed0ab53f4ebaa05036396c57f6e587f33d49667c1003cd03b71ad651b09dd4791bc631eaef93f1b313bbee7bd63a'
)
),
};

const publicKey = participantPublicKeys[address];
if (!publicKey) {
throw new Error(`No public key for participant: ${address}`);
}
return publicKey;
};
}
5 changes: 5 additions & 0 deletions src/policies/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ export class BlockchainPolicy {
public async generatePreEnactedPolicy(
ursulas: readonly Ursula[]
): Promise<PreEnactedPolicy> {
if (ursulas.length != this.verifiedKFrags.length) {
throw new Error(
`Number of ursulas must match number of verified kFrags: ${this.verifiedKFrags.length}`
);
}
const treasureMap = this.makeTreasureMap(ursulas, this.verifiedKFrags);
const encryptedTreasureMap = this.encryptTreasureMap(treasureMap);
// const revocationKit = new RevocationKit(treasureMap, this.publisher.signer);
Expand Down
Loading

0 comments on commit 1ad1ad9

Please sign in to comment.