404 lines
10 KiB
TypeScript
404 lines
10 KiB
TypeScript
/**
|
|
* SessionState Entity
|
|
*
|
|
* Tracks the state of an MPC session from this party's perspective.
|
|
* Used for monitoring and recovery purposes.
|
|
*/
|
|
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import { SessionId, PartyId, PublicKey, MessageHash, Signature } from '../value-objects';
|
|
import { SessionType, SessionStatus, ParticipantStatus } from '../enums';
|
|
import {
|
|
DomainEvent,
|
|
PartyJoinedSessionEvent,
|
|
KeygenCompletedEvent,
|
|
SigningCompletedEvent,
|
|
SessionFailedEvent,
|
|
SessionTimeoutEvent,
|
|
} from '../events';
|
|
|
|
export interface Participant {
|
|
partyId: string;
|
|
partyIndex: number;
|
|
status: ParticipantStatus;
|
|
}
|
|
|
|
export interface SessionStateCreateParams {
|
|
sessionId: SessionId;
|
|
partyId: PartyId;
|
|
partyIndex: number;
|
|
sessionType: SessionType;
|
|
participants: Participant[];
|
|
thresholdN: number;
|
|
thresholdT: number;
|
|
publicKey?: PublicKey;
|
|
messageHash?: MessageHash;
|
|
}
|
|
|
|
export interface SessionStateReconstructParams extends SessionStateCreateParams {
|
|
id: string;
|
|
status: SessionStatus;
|
|
currentRound: number;
|
|
errorMessage?: string;
|
|
signature?: Signature;
|
|
startedAt: Date;
|
|
completedAt?: Date;
|
|
}
|
|
|
|
export class SessionState {
|
|
// ============================================================================
|
|
// Private Fields
|
|
// ============================================================================
|
|
private readonly _id: string;
|
|
private readonly _sessionId: SessionId;
|
|
private readonly _partyId: PartyId;
|
|
private readonly _partyIndex: number;
|
|
private readonly _sessionType: SessionType;
|
|
private readonly _participants: Participant[];
|
|
private readonly _thresholdN: number;
|
|
private readonly _thresholdT: number;
|
|
private _status: SessionStatus;
|
|
private _currentRound: number;
|
|
private _errorMessage?: string;
|
|
private _publicKey?: PublicKey;
|
|
private _messageHash?: MessageHash;
|
|
private _signature?: Signature;
|
|
private readonly _startedAt: Date;
|
|
private _completedAt?: Date;
|
|
|
|
// Domain events collection
|
|
private readonly _domainEvents: DomainEvent[] = [];
|
|
|
|
// ============================================================================
|
|
// Constructor (private - use factory methods)
|
|
// ============================================================================
|
|
private constructor(
|
|
id: string,
|
|
sessionId: SessionId,
|
|
partyId: PartyId,
|
|
partyIndex: number,
|
|
sessionType: SessionType,
|
|
participants: Participant[],
|
|
thresholdN: number,
|
|
thresholdT: number,
|
|
status: SessionStatus,
|
|
currentRound: number,
|
|
startedAt: Date,
|
|
errorMessage?: string,
|
|
publicKey?: PublicKey,
|
|
messageHash?: MessageHash,
|
|
signature?: Signature,
|
|
completedAt?: Date,
|
|
) {
|
|
this._id = id;
|
|
this._sessionId = sessionId;
|
|
this._partyId = partyId;
|
|
this._partyIndex = partyIndex;
|
|
this._sessionType = sessionType;
|
|
this._participants = participants;
|
|
this._thresholdN = thresholdN;
|
|
this._thresholdT = thresholdT;
|
|
this._status = status;
|
|
this._currentRound = currentRound;
|
|
this._startedAt = startedAt;
|
|
this._errorMessage = errorMessage;
|
|
this._publicKey = publicKey;
|
|
this._messageHash = messageHash;
|
|
this._signature = signature;
|
|
this._completedAt = completedAt;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Factory Methods
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Create a new session state when joining a session
|
|
*/
|
|
static create(params: SessionStateCreateParams): SessionState {
|
|
const session = new SessionState(
|
|
uuidv4(),
|
|
params.sessionId,
|
|
params.partyId,
|
|
params.partyIndex,
|
|
params.sessionType,
|
|
params.participants,
|
|
params.thresholdN,
|
|
params.thresholdT,
|
|
SessionStatus.IN_PROGRESS,
|
|
0,
|
|
new Date(),
|
|
undefined,
|
|
params.publicKey,
|
|
params.messageHash,
|
|
);
|
|
|
|
// Emit joined event
|
|
session.addDomainEvent(new PartyJoinedSessionEvent(
|
|
params.sessionId.value,
|
|
params.partyId.value,
|
|
params.partyIndex,
|
|
params.sessionType,
|
|
));
|
|
|
|
return session;
|
|
}
|
|
|
|
/**
|
|
* Reconstruct from persistence
|
|
*/
|
|
static reconstruct(params: SessionStateReconstructParams): SessionState {
|
|
return new SessionState(
|
|
params.id,
|
|
params.sessionId,
|
|
params.partyId,
|
|
params.partyIndex,
|
|
params.sessionType,
|
|
params.participants,
|
|
params.thresholdN,
|
|
params.thresholdT,
|
|
params.status,
|
|
params.currentRound,
|
|
params.startedAt,
|
|
params.errorMessage,
|
|
params.publicKey,
|
|
params.messageHash,
|
|
params.signature,
|
|
params.completedAt,
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Getters
|
|
// ============================================================================
|
|
get id(): string {
|
|
return this._id;
|
|
}
|
|
|
|
get sessionId(): SessionId {
|
|
return this._sessionId;
|
|
}
|
|
|
|
get partyId(): PartyId {
|
|
return this._partyId;
|
|
}
|
|
|
|
get partyIndex(): number {
|
|
return this._partyIndex;
|
|
}
|
|
|
|
get sessionType(): SessionType {
|
|
return this._sessionType;
|
|
}
|
|
|
|
get participants(): Participant[] {
|
|
return [...this._participants];
|
|
}
|
|
|
|
get thresholdN(): number {
|
|
return this._thresholdN;
|
|
}
|
|
|
|
get thresholdT(): number {
|
|
return this._thresholdT;
|
|
}
|
|
|
|
get status(): SessionStatus {
|
|
return this._status;
|
|
}
|
|
|
|
get currentRound(): number {
|
|
return this._currentRound;
|
|
}
|
|
|
|
get errorMessage(): string | undefined {
|
|
return this._errorMessage;
|
|
}
|
|
|
|
get publicKey(): PublicKey | undefined {
|
|
return this._publicKey;
|
|
}
|
|
|
|
get messageHash(): MessageHash | undefined {
|
|
return this._messageHash;
|
|
}
|
|
|
|
get signature(): Signature | undefined {
|
|
return this._signature;
|
|
}
|
|
|
|
get startedAt(): Date {
|
|
return this._startedAt;
|
|
}
|
|
|
|
get completedAt(): Date | undefined {
|
|
return this._completedAt;
|
|
}
|
|
|
|
get domainEvents(): DomainEvent[] {
|
|
return [...this._domainEvents];
|
|
}
|
|
|
|
// ============================================================================
|
|
// Business Methods
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Update the current round number
|
|
*/
|
|
advanceRound(roundNumber: number): void {
|
|
this.ensureInProgress();
|
|
if (roundNumber <= this._currentRound) {
|
|
throw new Error(`Cannot go back to round ${roundNumber} from ${this._currentRound}`);
|
|
}
|
|
this._currentRound = roundNumber;
|
|
}
|
|
|
|
/**
|
|
* Mark keygen as completed
|
|
*/
|
|
completeKeygen(publicKey: PublicKey, shareId: string): void {
|
|
this.ensureInProgress();
|
|
if (this._sessionType !== SessionType.KEYGEN) {
|
|
throw new Error('Cannot complete keygen for non-keygen session');
|
|
}
|
|
|
|
this._publicKey = publicKey;
|
|
this._status = SessionStatus.COMPLETED;
|
|
this._completedAt = new Date();
|
|
|
|
this.addDomainEvent(new KeygenCompletedEvent(
|
|
this._sessionId.value,
|
|
this._partyId.value,
|
|
publicKey.toHex(),
|
|
shareId,
|
|
`${this._thresholdT}-of-${this._thresholdN}`,
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Mark signing as completed
|
|
*/
|
|
completeSigning(signature: Signature): void {
|
|
this.ensureInProgress();
|
|
if (this._sessionType !== SessionType.SIGN) {
|
|
throw new Error('Cannot complete signing for non-signing session');
|
|
}
|
|
if (!this._messageHash) {
|
|
throw new Error('Message hash not set for signing session');
|
|
}
|
|
if (!this._publicKey) {
|
|
throw new Error('Public key not set for signing session');
|
|
}
|
|
|
|
this._signature = signature;
|
|
this._status = SessionStatus.COMPLETED;
|
|
this._completedAt = new Date();
|
|
|
|
this.addDomainEvent(new SigningCompletedEvent(
|
|
this._sessionId.value,
|
|
this._partyId.value,
|
|
this._messageHash.toHex(),
|
|
signature.toHex(),
|
|
this._publicKey.toHex(),
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Mark session as failed
|
|
*/
|
|
fail(errorMessage: string, errorCode?: string): void {
|
|
if (this._status === SessionStatus.COMPLETED) {
|
|
throw new Error('Cannot fail a completed session');
|
|
}
|
|
|
|
this._status = SessionStatus.FAILED;
|
|
this._errorMessage = errorMessage;
|
|
this._completedAt = new Date();
|
|
|
|
this.addDomainEvent(new SessionFailedEvent(
|
|
this._sessionId.value,
|
|
this._partyId.value,
|
|
this._sessionType,
|
|
errorMessage,
|
|
errorCode,
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Mark session as timed out
|
|
*/
|
|
timeout(): void {
|
|
if (this._status === SessionStatus.COMPLETED) {
|
|
throw new Error('Cannot timeout a completed session');
|
|
}
|
|
|
|
this._status = SessionStatus.TIMEOUT;
|
|
this._completedAt = new Date();
|
|
|
|
this.addDomainEvent(new SessionTimeoutEvent(
|
|
this._sessionId.value,
|
|
this._partyId.value,
|
|
this._sessionType,
|
|
this._currentRound,
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Update participant status
|
|
*/
|
|
updateParticipantStatus(partyId: string, status: ParticipantStatus): void {
|
|
const participant = this._participants.find(p => p.partyId === partyId);
|
|
if (!participant) {
|
|
throw new Error(`Participant ${partyId} not found`);
|
|
}
|
|
participant.status = status;
|
|
}
|
|
|
|
/**
|
|
* Check if session is in progress
|
|
*/
|
|
isInProgress(): boolean {
|
|
return this._status === SessionStatus.IN_PROGRESS;
|
|
}
|
|
|
|
/**
|
|
* Check if session is completed
|
|
*/
|
|
isCompleted(): boolean {
|
|
return this._status === SessionStatus.COMPLETED;
|
|
}
|
|
|
|
/**
|
|
* Get the duration of the session in milliseconds
|
|
*/
|
|
getDuration(): number | undefined {
|
|
if (!this._completedAt) {
|
|
return Date.now() - this._startedAt.getTime();
|
|
}
|
|
return this._completedAt.getTime() - this._startedAt.getTime();
|
|
}
|
|
|
|
// ============================================================================
|
|
// Domain Event Management
|
|
// ============================================================================
|
|
|
|
addDomainEvent(event: DomainEvent): void {
|
|
this._domainEvents.push(event);
|
|
}
|
|
|
|
clearDomainEvents(): void {
|
|
this._domainEvents.length = 0;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Private Helper Methods
|
|
// ============================================================================
|
|
|
|
private ensureInProgress(): void {
|
|
if (this._status !== SessionStatus.IN_PROGRESS) {
|
|
throw new Error(`Cannot perform operation on ${this._status} session`);
|
|
}
|
|
}
|
|
}
|