164 lines
6.8 KiB
TypeScript
164 lines
6.8 KiB
TypeScript
import { pubkey, signer, nip44EncryptToSelf } from "@welshman/app"
|
|
import type { Hex, QuorumMember } from "./protocol"
|
|
|
|
// ── Quorum & shard records ────────────────────────────────────────────────────
|
|
|
|
/** A known quorum — no secret material */
|
|
export type QuorumRecord = {
|
|
quorumPubkey: Hex
|
|
members: QuorumMember[]
|
|
threshold: number
|
|
/** Feldman commitments per participant index, from the last completed DKG or resharing */
|
|
commitments: Record<number, Hex[]>
|
|
/** Ordered list of kind 7057 inner event IDs (rotation history) */
|
|
rotationRecords: Hex[]
|
|
}
|
|
|
|
/** The user's secret share for one quorum, with the shard encrypted at rest */
|
|
export type ShardRecord = {
|
|
quorumPubkey: Hex
|
|
index: number
|
|
verificationShare: Hex
|
|
/** NIP-44 self-encrypted hex string of the shard bigint */
|
|
encryptedShard: string
|
|
}
|
|
|
|
// ── Display model ─────────────────────────────────────────────────────────────
|
|
|
|
export type QuorumStatusKind = "complete" | "sending" | "failed" | "pending"
|
|
|
|
/**
|
|
* A quorum as shown in the UI — either a completed quorum or a still-pending DKG
|
|
* invite. Derived from protocol events in the repository, so it appears as soon as
|
|
* the invite is created (optimistically) and persists across reloads, even if the
|
|
* invite was never delivered to any relay.
|
|
*/
|
|
export type DisplayedQuorum = {
|
|
/** Stable id used for selection: the quorum pubkey once complete, else the invite id */
|
|
id: string
|
|
/** Set once DKG completes */
|
|
quorumPubkey?: Hex
|
|
/** The kind 7050 invite id (always set; the origin of the quorum) */
|
|
inviteId: Hex
|
|
members: QuorumMember[]
|
|
threshold: number
|
|
complete: boolean
|
|
status: QuorumStatusKind
|
|
statusLabel: string
|
|
/** Members who have broadcast round 1 (the creator always counts) */
|
|
joined: number
|
|
declined: number
|
|
createdAt: number
|
|
}
|
|
|
|
// ── Session helpers ───────────────────────────────────────────────────────────
|
|
|
|
/** Proof-of-knowledge from a round-1 message */
|
|
export type PoKProof = { R: Hex; s: Hex }
|
|
|
|
/** Round-1 broadcast from one participant */
|
|
export type Round1Data = { commitments: Hex[]; proof: PoKProof }
|
|
|
|
// ── DKG session ───────────────────────────────────────────────────────────────
|
|
|
|
export type DkgPhase = "round1" | "round2" | "confirming" | "complete"
|
|
|
|
/** In-progress DKG session, keyed by the kind 7050 inner event ID */
|
|
export type DkgSession = {
|
|
inviteId: Hex
|
|
members: Hex[]
|
|
threshold: number
|
|
phase: DkgPhase
|
|
/** Own Feldman commitments (set once we broadcast round 1) */
|
|
myCommitments?: Hex[]
|
|
/** Own polynomial coefficients, NIP-44 self-encrypted */
|
|
myEncryptedPoly?: string
|
|
/** Round-1 data received, keyed by sender pubkey */
|
|
round1: Record<Hex, Round1Data>
|
|
/** Round-2 shares received, keyed by sender participant index */
|
|
round2: Record<number, Hex>
|
|
/** Members who declined, keyed by pubkey, value is their optional reason */
|
|
declines: Record<Hex, string>
|
|
/** Set when the session was aborted (equivocation or a crypto-verification failure) */
|
|
aborted?: boolean
|
|
/** True once we have locally finalized and saved this quorum (usable for signing) */
|
|
complete?: boolean
|
|
/** Count of matching kind 7053 confirmations received (ours + peers') */
|
|
confirmed?: number
|
|
}
|
|
|
|
// ── Resharing session ─────────────────────────────────────────────────────────
|
|
|
|
export type ResharingPhase = "round1" | "round2" | "confirming" | "complete"
|
|
|
|
/** In-progress resharing session, keyed by the kind 7054 inner event ID */
|
|
export type ResharingSession = {
|
|
proposalId: Hex
|
|
quorumPubkey: Hex
|
|
contributors: Hex[]
|
|
newMembers: Hex[]
|
|
newThreshold: number
|
|
phase: ResharingPhase
|
|
myCommitments?: Hex[]
|
|
myEncryptedPoly?: string
|
|
/** Round-1 data received, keyed by sender pubkey */
|
|
round1: Record<Hex, Round1Data>
|
|
/** Round-2 shares received, keyed by sender participant index */
|
|
round2: Record<number, Hex>
|
|
/** Members who declined, keyed by pubkey, value is their optional reason */
|
|
declines: Record<Hex, string>
|
|
}
|
|
|
|
// ── Signing session ───────────────────────────────────────────────────────────
|
|
|
|
export type SigningPhase = "round1" | "round2" | "complete"
|
|
|
|
/** In-progress signing session, keyed by the kind 7058 inner event ID */
|
|
export type SigningSession = {
|
|
requestId: Hex
|
|
quorumPubkey: Hex
|
|
/** Hex-encoded message bytes */
|
|
msgHex: Hex
|
|
phase: SigningPhase
|
|
signingSet: number[]
|
|
/** Own nonce commitments (present until we have broadcast our round-2 share) */
|
|
myNonces?: { D: Hex; E: Hex }
|
|
/** Round-1 nonce commitments from others, keyed by sender pubkey */
|
|
round1: Record<Hex, { D: Hex; E: Hex }>
|
|
/** Round-2 signature shares, keyed by participant index */
|
|
shares: Record<number, Hex>
|
|
/** Members who declined, keyed by pubkey, value is their optional reason */
|
|
declines: Record<Hex, string>
|
|
}
|
|
|
|
// ── Shard / polynomial encryption ────────────────────────────────────────────
|
|
// Shards and polynomials are self-encrypted via the active session's signer:
|
|
// NIP-44 encrypt(myPubkey, ...). The signer derives the conversation key from
|
|
// (myPrivkey, myPubkey), which is unique to the key and opaque to any other
|
|
// party. The signer store is typed non-optional but is undefined at runtime
|
|
// when there is no active session, hence the guards.
|
|
|
|
export async function encryptShard(shard: bigint): Promise<string> {
|
|
return nip44EncryptToSelf(shard.toString(16))
|
|
}
|
|
|
|
export async function decryptShard(encryptedShard: string): Promise<bigint> {
|
|
const $pubkey = pubkey.get()
|
|
const $signer = signer.get()
|
|
if (!$pubkey || !$signer) { throw new Error("Cannot decrypt without an active signer") }
|
|
const hex = await $signer.nip44.decrypt($pubkey, encryptedShard)
|
|
return BigInt("0x" + hex)
|
|
}
|
|
|
|
export async function encryptPoly(poly: bigint[]): Promise<string> {
|
|
return nip44EncryptToSelf(JSON.stringify(poly.map(c => c.toString(16))))
|
|
}
|
|
|
|
export async function decryptPoly(encryptedPoly: string): Promise<bigint[]> {
|
|
const $pubkey = pubkey.get()
|
|
const $signer = signer.get()
|
|
if (!$pubkey || !$signer) { throw new Error("Cannot decrypt without an active signer") }
|
|
const json = await $signer.nip44.decrypt($pubkey, encryptedPoly)
|
|
return (JSON.parse(json) as string[]).map(h => BigInt("0x" + h))
|
|
}
|