Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timeout #251

Closed
wants to merge 12 commits into from
89 changes: 83 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,93 @@ 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 getTimeout(
provider: ethers.providers.Web3Provider
): Promise<number> {
const Coordinator = await this.connectReadOnly(provider);
const timeout = await Coordinator.timeout();
return timeout;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timeout is the number of seconds counted since the ritual started, right? What happens if my transaction gets stuck (low gas etc.) but I'm still counting from when I send the transaction? Should we instead count the timeout from the "ritual start" event or some other cut-off?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, timeout is a variable on the Coordinator https://github.com/nucypher/nucypher-contracts/blob/da35b9d9f13ddebd7ce7bbbc3fbc7d5fb0d95411/contracts/contracts/coordination/Coordinator.sol#L61

It's set at Coordinator level and applies to all rituals

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm missing something here. Timeout is defined as ritual.initTimestamp + timeout < block.timestamp, and the initial value of ritual.initTimestamp is taken from block.timestamp. So it seems to me like the timeout denotes some number of seconds, and the timeout occurs after we reach some number of seconds after the ritual started.

Do you see any edge cases here? If we use setTimeout, is it going to match exactly the calculation performed in ritual state checks? I.e. ritual.initTimestamp + timeout < block.timestamp

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timeout denotes some number of seconds, and the timeout occurs after we reach some number of seconds after the ritual started.
Yes, agree

setTimeout is changing the config of Coordinator, so not sure that it helps us.

But i see what your original comment implied now. When we do:

const ritualId = await DkgCoordinatorAgent.initializeRitual(
      web3Provider,
      ursulas.sort()
    );

The fact that we await doesn't always mean that the transaction went through? and it could therefore be sitting around in the mempool whilst the Promise.race starts counting down. Is that correct?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initializeRitual is blocked until the ritual is started (and some event is emitted, etc.) so it may not be an issue after all.

I was trying to give a concrete example of something that @manumonti already hinted on in his comment:

Thinking aloud, since the "timer" that runs in nucypher-ts and the "timer" than runs in Coordinator contract are not the same and probably, due to the network delays, one of them will reach the deadline before the other... I wonder if the difference between these two deadlines is significant or not

I didn't make a full analysis on what are the different edge cases we may have here, but one other that comes to mind is where we're awaiting for timeout and the Coordinator admin changes the value of timeout. I think this case and some basket of other cases can be handled by replacing setTimeout by setInterval and by redoing the contract calculation ritual.initTimestamp + timeout < block.timestamp in JS. Or alternatively, querying the contract for the ritual state. The latter seems to be more robust.

}

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 getRitualInitTime(
provider: ethers.providers.Web3Provider,
ritualId: number
): Promise<number> {
const Coordinator = await this.connectReadOnly(provider);
const ritual = await Coordinator.rituals(ritualId);
return ritual[2];
}

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
8 changes: 5 additions & 3 deletions src/agents/subscription-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class PreSubscriptionManagerAgent {
}

public static async getPolicyCost(
provider: ethers.providers.Provider,
provider: ethers.providers.Web3Provider,
size: number,
startTimestamp: number,
endTimestamp: number
Expand All @@ -61,7 +61,9 @@ export class PreSubscriptionManagerAgent {
);
}

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

Expand All @@ -72,7 +74,7 @@ export class PreSubscriptionManagerAgent {
}

private static async connect(
provider: ethers.providers.Provider,
provider: ethers.providers.Web3Provider,
signer?: ethers.providers.JsonRpcSigner
): Promise<SubscriptionManager> {
const network = await provider.getNetwork();
Expand Down
46 changes: 38 additions & 8 deletions src/characters/cbd-recipient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ import {
} from '@nucypher/nucypher-core';
import { ethers } from 'ethers';

import { DkgCoordinatorAgent, DkgParticipant } from '../agents/coordinator';
import {
DkgCoordinatorAgent,
DkgParticipant,
DkgRitualState,
} from '../agents/coordinator';
import { ConditionExpression } from '../conditions';
import {
DkgClient,
DkgRitual,
FerveoVariant,
getCombineDecryptionSharesFunction,
Expand Down Expand Up @@ -43,7 +48,7 @@ export class CbdTDecDecrypter {
return new CbdTDecDecrypter(
new Porter(porterUri),
dkgRitual.id,
dkgRitual.threshold
dkgRitual.dkgParams.threshold
);
}

Expand All @@ -52,13 +57,15 @@ export class CbdTDecDecrypter {
provider: ethers.providers.Web3Provider,
conditionExpr: ConditionExpression,
variant: FerveoVariant,
ciphertext: Ciphertext
ciphertext: Ciphertext,
verifyRitual = true
): Promise<Uint8Array> {
const decryptionShares = await this.retrieve(
provider,
conditionExpr,
variant,
ciphertext
ciphertext,
verifyRitual
);

const combineDecryptionSharesFn =
Expand All @@ -73,16 +80,39 @@ export class CbdTDecDecrypter {

// Retrieve decryption shares
public async retrieve(
provider: ethers.providers.Web3Provider,
web3Provider: ethers.providers.Web3Provider,
conditionExpr: ConditionExpression,
variant: number,
ciphertext: Ciphertext
ciphertext: Ciphertext,
verifyRitual = true
): Promise<DecryptionSharePrecomputed[] | DecryptionShareSimple[]> {
const ritualState = await DkgCoordinatorAgent.getRitualState(
web3Provider,
this.ritualId
);
if (ritualState !== DkgRitualState.FINALIZED) {
throw new Error(
`Ritual with id ${this.ritualId} is not finalized. Ritual state is ${ritualState}.`
);
}

if (verifyRitual) {
const isLocallyVerified = await DkgClient.verifyRitual(
web3Provider,
this.ritualId
);
if (!isLocallyVerified) {
throw new Error(
`Ritual with id ${this.ritualId} has failed local verification.`
);
}
}

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,
variant,
Expand Down
Loading
Loading