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
JavaScript
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();
}
}