UNPKG

lotus-sdk

Version:

Central repository for several classes of tools for integrating with, and building for, the Lotusia ecosystem

324 lines (323 loc) 13.2 kB
import { EventEmitter } from 'events'; import { DHTAdvertiser } from '../discovery/dht-advertiser.js'; import { DHTDiscoverer } from '../discovery/dht-discoverer.js'; import { DiscoveryError, DiscoveryErrorType, } from '../discovery/types.js'; import { DEFAULT_MUSIG2_DISCOVERY_CONFIG, isValidSignerAdvertisement, isValidSigningRequestAdvertisement, publicKeyToHex, } from './discovery-types.js'; import { MuSig2Event } from './types.js'; import { sha256 } from '@noble/hashes/sha256'; import { bytesToHex } from '@noble/hashes/utils'; export class MuSig2Discovery extends EventEmitter { advertiser; discoverer; config; coordinator; activeSignerAd = null; activeRequestAds = new Map(); refreshTimer = null; constructor(coordinator, config) { super(); this.coordinator = coordinator; this.config = { ...DEFAULT_MUSIG2_DISCOVERY_CONFIG, ...config }; this.advertiser = new DHTAdvertiser(coordinator); this.discoverer = new DHTDiscoverer(coordinator); } async start() { await this.advertiser.start(); await this.discoverer.start(); if (this.config.enableAutoRefresh && this.config.signerRefreshInterval > 0) { this.refreshTimer = setInterval(() => { this.refreshActiveAdvertisements().catch(err => { this.emit('error', new DiscoveryError(DiscoveryErrorType.CONFIGURATION_ERROR, `Auto-refresh failed: ${err.message}`, err)); }); }, this.config.signerRefreshInterval); } } async stop() { if (this.refreshTimer) { clearInterval(this.refreshTimer); this.refreshTimer = null; } await this.withdrawAllAdvertisements(); await this.advertiser.stop(); await this.discoverer.stop(); } async advertiseSigner(publicKey, transactionTypes, options) { if (this.activeSignerAd) { throw new DiscoveryError(DiscoveryErrorType.INVALID_ADVERTISEMENT, 'Signer already advertised. Withdraw first before re-advertising.'); } const publicKeyHex = publicKeyToHex(publicKey); const advertisementId = this.generateSignerAdId(publicKeyHex); const advertisement = { id: advertisementId, protocol: 'musig2', peerInfo: { peerId: this.coordinator.peerId, multiaddrs: this.coordinator.libp2pNode .getMultiaddrs() .map(ma => ma.toString()), }, publicKey, transactionTypes, amountRange: options?.amountRange, signerMetadata: options?.metadata, capabilities: ['musig2-signer', ...transactionTypes], createdAt: Date.now(), expiresAt: Date.now() + (options?.ttl || this.config.signerTTL), reputation: 50, customCriteria: { transactionTypes, publicKeyHex, }, }; const discoveryOptions = { ttl: options?.ttl || this.config.signerTTL, autoRefresh: this.config.enableAutoRefresh, refreshInterval: this.config.signerRefreshInterval, }; const adId = await this.advertiser.advertise(advertisement, discoveryOptions); this.activeSignerAd = adId; this.emit(MuSig2Event.SIGNER_ADVERTISED, advertisement); return adId; } async withdrawSigner() { if (!this.activeSignerAd) { return; } await this.advertiser.withdraw(this.activeSignerAd); this.activeSignerAd = null; this.emit(MuSig2Event.SIGNER_WITHDRAWN); } async discoverSigners(criteria = {}, options) { const fullCriteria = { protocol: 'musig2', ...criteria, }; const results = await this.discoverer.discover(fullCriteria, options); const signerAds = []; for (const ad of results) { if (this.isSignerAdvertisement(ad)) { if (this.matchesSignerCriteria(ad, fullCriteria)) { signerAds.push(ad); this.emit(MuSig2Event.SIGNER_DISCOVERED, ad); } } } return signerAds; } async createSigningRequest(requiredPublicKeys, messageHash, options) { if (this.activeRequestAds.size >= this.config.maxConcurrentRequests) { throw new DiscoveryError(DiscoveryErrorType.RATE_LIMIT_EXCEEDED, `Maximum concurrent requests (${this.config.maxConcurrentRequests}) exceeded`); } const requestId = this.generateRequestId(requiredPublicKeys, messageHash); const peerId = this.coordinator.peerId; const creatorPublicKey = requiredPublicKeys[0]; const advertisement = { id: requestId, requestId, protocol: 'musig2-request', peerInfo: { peerId, multiaddrs: this.coordinator.libp2pNode .getMultiaddrs() .map(ma => ma.toString()), }, requiredPublicKeys: requiredPublicKeys.map(pk => publicKeyToHex(pk)), messageHash, creatorPeerId: peerId, creatorPublicKey: publicKeyToHex(creatorPublicKey), requestMetadata: options?.metadata, creatorSignature: options?.creatorSignature || Buffer.alloc(0), capabilities: ['musig2-signing-request'], createdAt: Date.now(), expiresAt: Date.now() + (options?.ttl || this.config.requestTTL), reputation: 50, customCriteria: { requiredPublicKeys: requiredPublicKeys.map(pk => publicKeyToHex(pk)), messageHash, }, }; const discoveryOptions = { ttl: options?.ttl || this.config.requestTTL, autoRefresh: false, }; const adId = await this.advertiser.advertise(advertisement, discoveryOptions); this.activeRequestAds.set(requestId, adId); this.emit(MuSig2Event.SIGNING_REQUEST_CREATED, advertisement); return requestId; } async withdrawSigningRequest(requestId) { const adId = this.activeRequestAds.get(requestId); if (!adId) { return; } await this.advertiser.withdraw(adId); this.activeRequestAds.delete(requestId); } async discoverSigningRequests(criteria = {}, options) { const fullCriteria = { protocol: 'musig2-request', ...criteria, }; const results = await this.discoverer.discover(fullCriteria, options); const requestAds = []; for (const ad of results) { if (this.isSigningRequestAdvertisement(ad)) { if (this.matchesRequestCriteria(ad, fullCriteria)) { requestAds.push(ad); this.emit(MuSig2Event.SIGNING_REQUEST_RECEIVED, ad); } } } return requestAds; } async joinSigningRequest(requestId, publicKey) { const requests = await this.discoverSigningRequests({ maxResults: 100 }); const request = requests.find(r => r.requestId === requestId); if (!request) { throw new DiscoveryError(DiscoveryErrorType.INVALID_CRITERIA, `Signing request ${requestId} not found`); } const publicKeyHex = publicKeyToHex(publicKey); if (!request.requiredPublicKeys.includes(publicKeyHex)) { throw new DiscoveryError(DiscoveryErrorType.SECURITY_VALIDATION_FAILED, `Public key ${publicKeyHex} not in required signers list`); } this.emit(MuSig2Event.SIGNING_REQUEST_JOINED, requestId); } generateSignerAdId(publicKeyHex) { const data = `musig2:signer:${publicKeyHex}${Date.now().toString()}`; const hash = bytesToHex(sha256(new TextEncoder().encode(data))); return `${this.config.signerKeyPrefix}${hash.substring(0, 32)}`; } generateRequestId(requiredPublicKeys, messageHash) { const keysStr = requiredPublicKeys .map(pk => publicKeyToHex(pk)) .sort() .join(':'); const data = `musig2:request:${messageHash}${keysStr}${Date.now().toString()}`; const hash = bytesToHex(sha256(new TextEncoder().encode(data))); return `${this.config.requestKeyPrefix}${hash.substring(0, 32)}`; } isSignerAdvertisement(ad) { return isValidSignerAdvertisement(ad); } isSigningRequestAdvertisement(ad) { return isValidSigningRequestAdvertisement(ad); } matchesSignerCriteria(ad, criteria) { if (criteria.transactionTypes) { const hasMatchingType = criteria.transactionTypes.some(type => ad.transactionTypes.includes(type)); if (!hasMatchingType) return false; } if (criteria.minAmount !== undefined && ad.amountRange?.max !== undefined) { if (ad.amountRange.max < criteria.minAmount) return false; } if (criteria.maxAmount !== undefined && ad.amountRange?.min !== undefined) { if (ad.amountRange.min > criteria.maxAmount) return false; } if (criteria.requiredPublicKeys) { const publicKeyHex = publicKeyToHex(ad.publicKey); if (!criteria.requiredPublicKeys.includes(publicKeyHex)) return false; } if (criteria.minMaturation !== undefined && ad.signerMetadata?.identity?.maturationBlocks !== undefined) { if (ad.signerMetadata.identity.maturationBlocks < criteria.minMaturation) return false; } if (criteria.minTotalBurned !== undefined && ad.signerMetadata?.identity?.totalBurned !== undefined) { if (ad.signerMetadata.identity.totalBurned < criteria.minTotalBurned) return false; } return true; } matchesRequestCriteria(ad, criteria) { if (criteria.transactionType && ad.requestMetadata?.transactionType !== criteria.transactionType) { return false; } if (criteria.amountRange) { const amount = ad.requestMetadata?.amount; if (amount === undefined) return false; if (criteria.amountRange.min !== undefined && amount < criteria.amountRange.min) return false; if (criteria.amountRange.max !== undefined && amount > criteria.amountRange.max) return false; } if (criteria.includesPublicKeys) { const hasAllKeys = criteria.includesPublicKeys.every(key => ad.requiredPublicKeys.includes(key)); if (!hasAllKeys) return false; } if (criteria.creatorPeerIds) { if (!criteria.creatorPeerIds.includes(ad.creatorPeerId)) return false; } if (criteria.expiresAfter !== undefined && ad.expiresAt < criteria.expiresAfter) { return false; } return true; } async refreshActiveAdvertisements() { if (this.activeSignerAd) { await this.advertiser.refresh(this.activeSignerAd); } for (const adId of this.activeRequestAds.values()) { await this.advertiser.refresh(adId); } } async withdrawAllAdvertisements() { if (this.activeSignerAd) { await this.withdrawSigner(); } for (const requestId of Array.from(this.activeRequestAds.keys())) { await this.withdrawSigningRequest(requestId); } } async subscribeToSigners(criteria = {}, callback) { const fullCriteria = { protocol: 'musig2', ...criteria, }; const subscription = await this.discoverer.subscribe(fullCriteria, (ad) => { if (this.isSignerAdvertisement(ad)) { if (this.matchesSignerCriteria(ad, fullCriteria)) { this.emit(MuSig2Event.SIGNER_DISCOVERED, ad); callback(ad); } } }, { fetchExisting: true, deduplicate: true, }); return subscription.id; } async subscribeToSigningRequests(criteria = {}, callback) { const fullCriteria = { protocol: 'musig2-request', ...criteria, }; const subscription = await this.discoverer.subscribe(fullCriteria, (ad) => { if (this.isSigningRequestAdvertisement(ad)) { if (this.matchesRequestCriteria(ad, fullCriteria)) { this.emit(MuSig2Event.SIGNING_REQUEST_RECEIVED, ad); callback(ad); } } }, { fetchExisting: true, deduplicate: true, }); return subscription.id; } async unsubscribe(subscriptionId) { await this.discoverer.unsubscribe(subscriptionId); } }