UNPKG

lotus-sdk

Version:

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

295 lines (294 loc) 13.6 kB
import { EventEmitter } from 'events'; import { Hash } from '../../bitcore/crypto/hash.js'; import { Schnorr } from '../../bitcore/crypto/schnorr.js'; import { Signature } from '../../bitcore/crypto/signature.js'; import { BN } from '../../bitcore/crypto/bn.js'; import { BurnVerifier } from '../blockchain-utils.js'; import { MUSIG2_LOKAD, MUSIG2_BURN_REQUIREMENTS, MUSIG2_MATURATION_PERIODS, } from './types.js'; export class MuSig2IdentityManager extends EventEmitter { burnVerifier; identities = new Map(); publicKeyToIdentity = new Map(); bannedIdentities = new Set(); maturationPeriod; constructor(chronikUrl, maturationPeriod = MUSIG2_MATURATION_PERIODS.IDENTITY_REGISTRATION) { super(); this.burnVerifier = new BurnVerifier(chronikUrl); this.maturationPeriod = maturationPeriod; console.log(`[IdentityManager] Initialized with maturation period: ${maturationPeriod} blocks`); } async registerIdentity(txId, outputIndex, publicKey, signature) { try { const burnResult = await this.burnVerifier.verifyBurnTransaction(txId, outputIndex, this.maturationPeriod); if (!burnResult) { console.warn(`[IdentityManager] Burn verification failed for ${txId}:${outputIndex}`); return null; } if (!burnResult.isMatured) { console.warn(`[IdentityManager] Burn not matured yet: ${burnResult.confirmations} confirmations (need ${this.maturationPeriod}) for ${txId}:${outputIndex}`); return null; } if (burnResult.burnAmount < MUSIG2_BURN_REQUIREMENTS.IDENTITY_REGISTRATION) { console.warn(`[IdentityManager] Insufficient burn amount: ${burnResult.burnAmount} (need ${MUSIG2_BURN_REQUIREMENTS.IDENTITY_REGISTRATION})`); return null; } if (!burnResult.lokadPrefix?.equals(MUSIG2_LOKAD.PREFIX)) { console.warn(`[IdentityManager] Invalid LOKAD prefix: ${burnResult.lokadPrefix?.toString('hex')}`); return null; } if (burnResult.lokadVersion !== MUSIG2_LOKAD.VERSION) { console.warn(`[IdentityManager] Invalid LOKAD version: ${burnResult.lokadVersion}`); return null; } const payloadPubKey = this.burnVerifier.parsePublicKeyFromLokad(burnResult.lokadPayload); if (!payloadPubKey) { console.warn(`[IdentityManager] Failed to parse public key from LOKAD payload`); return null; } if (!publicKey.toBuffer().equals(payloadPubKey)) { console.warn(`[IdentityManager] Public key mismatch: provided ${publicKey.toString()} vs payload ${payloadPubKey.toString('hex')}`); return null; } const identityId = this.burnVerifier.deriveIdentityId(txId, outputIndex); if (this.identities.has(identityId)) { console.warn(`[IdentityManager] Identity already registered: ${identityId}`); return identityId; } const message = Buffer.from(identityId, 'hex'); const isValidSignature = this.verifySignature(message, signature, publicKey); if (!isValidSignature) { console.warn(`[IdentityManager] Invalid signature for identity ${identityId}`); return null; } const now = Date.now(); const identity = { identityId, burnProof: { txId, outputIndex, burnAmount: burnResult.burnAmount, burnHeight: burnResult.blockHeight, confirmations: burnResult.confirmations, }, identityCommitment: { publicKey, signature, timestamp: now, }, reputation: { identityId, score: 50, completedSignings: 0, failedSignings: 0, totalSignings: 0, averageResponseTime: 0, totalBurned: burnResult.burnAmount, firstSeen: now, lastUpdated: now, }, keyHistory: [ { publicKey: publicKey.toString(), activatedAt: now, }, ], registeredAt: now, lastVerified: now, }; this.identities.set(identityId, identity); this.publicKeyToIdentity.set(publicKey.toString(), identityId); console.log(`[IdentityManager] ✓ Registered identity ${identityId.slice(0, 20)}... with ${burnResult.confirmations} confirmations (matured)`); this.emit('identity:registered', identityId, identity.burnProof); return identityId; } catch (error) { console.error('[IdentityManager] Error registering identity:', error); return null; } } async rotateKey(identityId, oldPublicKey, newPublicKey, oldKeySignature, newKeySignature, rotationBurnTxId, rotationBurnOutputIndex) { try { const identity = this.identities.get(identityId); if (!identity) { console.warn(`[IdentityManager] Identity not found: ${identityId}`); return false; } if (identity.identityCommitment.publicKey.toString() !== oldPublicKey.toString()) { console.warn(`[IdentityManager] Current public key mismatch for identity ${identityId}`); return false; } const rotationMaturation = MUSIG2_MATURATION_PERIODS.KEY_ROTATION; const burnResult = await this.burnVerifier.verifyBurnTransaction(rotationBurnTxId, rotationBurnOutputIndex, rotationMaturation); if (!burnResult) { console.warn(`[IdentityManager] Rotation burn verification failed for identity ${identityId}`); return false; } if (!burnResult.isMatured) { console.warn(`[IdentityManager] Rotation burn not matured: ${burnResult.confirmations} confirmations (need ${rotationMaturation}) for identity ${identityId}`); return false; } if (burnResult.burnAmount < MUSIG2_BURN_REQUIREMENTS.KEY_ROTATION) { console.warn(`[IdentityManager] Insufficient rotation burn: ${burnResult.burnAmount} (need ${MUSIG2_BURN_REQUIREMENTS.KEY_ROTATION}) for identity ${identityId}`); return false; } const rotationMessage = Buffer.concat([ Buffer.from(identityId, 'hex'), Buffer.from('KEY_ROTATION', 'utf8'), newPublicKey.toBuffer(), ]); const isOldKeyValid = this.verifySignature(rotationMessage, oldKeySignature, oldPublicKey); if (!isOldKeyValid) { console.warn(`[IdentityManager] Invalid old key signature for identity ${identityId}`); return false; } const isNewKeyValid = this.verifySignature(rotationMessage, newKeySignature, newPublicKey); if (!isNewKeyValid) { console.warn(`[IdentityManager] Invalid new key signature for identity ${identityId}`); return false; } const now = Date.now(); const currentKeyEntry = identity.keyHistory[identity.keyHistory.length - 1]; currentKeyEntry.revokedAt = now; identity.keyHistory.push({ publicKey: newPublicKey.toString(), activatedAt: now, rotationSignature: newKeySignature, }); identity.identityCommitment = { publicKey: newPublicKey, signature: newKeySignature, timestamp: now, }; identity.reputation.totalBurned += burnResult.burnAmount; this.publicKeyToIdentity.delete(oldPublicKey.toString()); this.publicKeyToIdentity.set(newPublicKey.toString(), identityId); console.log(`[IdentityManager] Rotated key for identity ${identityId}: ${oldPublicKey.toString()} -> ${newPublicKey.toString()}`); this.emit('key:rotated', identityId, oldPublicKey.toString(), newPublicKey.toString()); return true; } catch (error) { console.error('[IdentityManager] Error rotating key:', error); return false; } } getIdentity(identityId) { return this.identities.get(identityId); } getIdentityByPublicKey(publicKey) { const identityId = this.publicKeyToIdentity.get(publicKey); if (!identityId) return undefined; return this.identities.get(identityId); } hasIdentity(identityId) { return this.identities.has(identityId); } isBanned(identityId) { return this.bannedIdentities.has(identityId); } isAllowed(identityId, minReputation = 0) { if (this.isBanned(identityId)) return false; const identity = this.getIdentity(identityId); if (!identity) return false; return identity.reputation.score >= minReputation; } verifyPublicKeyOwnership(identityId, publicKey) { const identity = this.getIdentity(identityId); if (!identity) return false; if (identity.identityCommitment.publicKey.toString() === publicKey) { return true; } return identity.keyHistory.some(entry => entry.publicKey === publicKey && !entry.revokedAt); } recordSuccessfulSigning(identityId, responseTimeMs) { const identity = this.getIdentity(identityId); if (!identity) return; const oldScore = identity.reputation.score; identity.reputation.completedSignings++; identity.reputation.totalSignings++; const totalResponses = identity.reputation.completedSignings; const currentAvg = identity.reputation.averageResponseTime; identity.reputation.averageResponseTime = (currentAvg * (totalResponses - 1) + responseTimeMs) / totalResponses; identity.reputation.score = Math.min(100, identity.reputation.score + 2); identity.reputation.lastUpdated = Date.now(); this.emit('reputation:updated', identityId, oldScore, identity.reputation.score); } recordFailedSigning(identityId, reason) { const identity = this.getIdentity(identityId); if (!identity) return; const oldScore = identity.reputation.score; identity.reputation.failedSignings++; identity.reputation.totalSignings++; identity.reputation.score = Math.max(0, identity.reputation.score - 5); identity.reputation.lastUpdated = Date.now(); console.log(`[IdentityManager] Failed signing for identity ${identityId}: ${reason} (new score: ${identity.reputation.score})`); this.emit('reputation:updated', identityId, oldScore, identity.reputation.score); if (identity.reputation.score === 0) { this.banIdentity(identityId, 'Reputation dropped to zero'); } } getReputation(identityId) { const identity = this.getIdentity(identityId); return identity?.reputation.score ?? 0; } getReputationData(identityId) { return this.getIdentity(identityId)?.reputation; } banIdentity(identityId, reason) { this.bannedIdentities.add(identityId); console.log(`[IdentityManager] Banned identity ${identityId}: ${reason}`); this.emit('identity:banned', identityId, reason); } unbanIdentity(identityId) { this.bannedIdentities.delete(identityId); console.log(`[IdentityManager] Unbanned identity ${identityId}`); } getIdentityCount() { return this.identities.size; } getTotalBurned() { let total = 0; for (const identity of this.identities.values()) { total += identity.reputation.totalBurned; } return total; } getAllIdentities() { return Array.from(this.identities.values()); } getIdentitiesWithMinReputation(minReputation) { return this.getAllIdentities().filter(identity => identity.reputation.score >= minReputation && !this.isBanned(identity.identityId)); } verifySignature(message, signatureBuffer, publicKey) { try { const hashbuf = Hash.sha256(message); if (signatureBuffer.length !== 64) { console.warn(`[IdentityManager] Invalid signature length: ${signatureBuffer.length}`); return false; } const signature = new Signature({ r: new BN(signatureBuffer.subarray(0, 32), 'be'), s: new BN(signatureBuffer.subarray(32, 64), 'be'), isSchnorr: true, }); return Schnorr.verify(hashbuf, signature, publicKey, 'big'); } catch (error) { console.error('[IdentityManager] Signature verification error:', error); return false; } } cleanup() { } shutdown() { this.removeAllListeners(); } }