lotus-sdk
Version:
Central repository for several classes of tools for integrating with, and building for, the Lotusia ecosystem
207 lines (206 loc) • 7.72 kB
JavaScript
import { sha256 } from '@noble/hashes/sha256';
import { bytesToHex } from '@noble/hashes/utils';
export var ElectionMethod;
(function (ElectionMethod) {
ElectionMethod["LEXICOGRAPHIC"] = "lexicographic";
ElectionMethod["HASH_BASED"] = "hash-based";
ElectionMethod["FIRST_SIGNER"] = "first-signer";
ElectionMethod["LAST_SIGNER"] = "last-signer";
})(ElectionMethod || (ElectionMethod = {}));
export function electCoordinator(signers, method = ElectionMethod.LEXICOGRAPHIC) {
if (signers.length === 0) {
throw new Error('Cannot elect coordinator: no signers provided');
}
if (signers.length === 1) {
return {
coordinatorIndex: 0,
coordinatorPublicKey: signers[0],
sortedSigners: [signers[0]],
indexMapping: new Map([[0, 0]]),
electionProof: computeElectionProof(signers),
};
}
switch (method) {
case ElectionMethod.LEXICOGRAPHIC:
return electByLexicographic(signers);
case ElectionMethod.HASH_BASED:
return electByHash(signers);
case ElectionMethod.FIRST_SIGNER:
return electFirstSigner(signers);
case ElectionMethod.LAST_SIGNER:
return electLastSigner(signers);
default:
throw new Error(`Unknown election method: ${method}`);
}
}
function electByLexicographic(signers) {
const indexed = signers.map((pk, idx) => ({
originalIndex: idx,
publicKey: pk,
buffer: pk.toBuffer(),
}));
indexed.sort((a, b) => a.buffer.compare(b.buffer));
const coordinator = indexed[0];
const sortedSigners = indexed.map(item => item.publicKey);
const indexMapping = new Map();
indexed.forEach((item, sortedIdx) => {
indexMapping.set(item.originalIndex, sortedIdx);
});
return {
coordinatorIndex: 0,
coordinatorPublicKey: coordinator.publicKey,
sortedSigners,
indexMapping,
electionProof: computeElectionProof(signers),
};
}
function electByHash(signers) {
const indexed = signers.map((pk, idx) => ({
originalIndex: idx,
publicKey: pk,
buffer: pk.toBuffer(),
}));
indexed.sort((a, b) => a.buffer.compare(b.buffer));
const sortedSigners = indexed.map(item => item.publicKey);
const indexMapping = new Map();
indexed.forEach((item, sortedIdx) => {
indexMapping.set(item.originalIndex, sortedIdx);
});
const concatenated = sortedSigners.map(pk => pk.toString()).join('');
const hash = Buffer.from(sha256(new TextEncoder().encode(concatenated)));
const hashValue = hash.readUInt32BE(0);
const coordinatorIndex = hashValue % sortedSigners.length;
return {
coordinatorIndex,
coordinatorPublicKey: sortedSigners[coordinatorIndex],
sortedSigners,
indexMapping,
electionProof: computeElectionProof(signers),
};
}
function electFirstSigner(signers) {
const indexed = signers.map((pk, idx) => ({
originalIndex: idx,
publicKey: pk,
buffer: pk.toBuffer(),
}));
indexed.sort((a, b) => a.buffer.compare(b.buffer));
const sortedSigners = indexed.map(item => item.publicKey);
const indexMapping = new Map();
indexed.forEach((item, sortedIdx) => {
indexMapping.set(item.originalIndex, sortedIdx);
});
return {
coordinatorIndex: 0,
coordinatorPublicKey: sortedSigners[0],
sortedSigners,
indexMapping,
electionProof: computeElectionProof(signers),
};
}
function electLastSigner(signers) {
const indexed = signers.map((pk, idx) => ({
originalIndex: idx,
publicKey: pk,
buffer: pk.toBuffer(),
}));
indexed.sort((a, b) => a.buffer.compare(b.buffer));
const sortedSigners = indexed.map(item => item.publicKey);
const coordinatorIndex = sortedSigners.length - 1;
const indexMapping = new Map();
indexed.forEach((item, sortedIdx) => {
indexMapping.set(item.originalIndex, sortedIdx);
});
return {
coordinatorIndex,
coordinatorPublicKey: sortedSigners[coordinatorIndex],
sortedSigners,
indexMapping,
electionProof: computeElectionProof(signers),
};
}
function computeElectionProof(signers) {
const concatenated = signers
.map(pk => pk.toString())
.sort()
.join('');
return bytesToHex(sha256(new TextEncoder().encode(concatenated)));
}
export function verifyElectionResult(signers, result, method = ElectionMethod.LEXICOGRAPHIC) {
const expectedResult = electCoordinator(signers, method);
return (expectedResult.coordinatorIndex === result.coordinatorIndex &&
expectedResult.electionProof === result.electionProof);
}
export function isCoordinator(signers, signerIndex, method = ElectionMethod.LEXICOGRAPHIC) {
if (signerIndex < 0 || signerIndex >= signers.length) {
return false;
}
const election = electCoordinator(signers, method);
return election.coordinatorIndex === signerIndex;
}
export function getCoordinatorPublicKey(signers, method = ElectionMethod.LEXICOGRAPHIC) {
const election = electCoordinator(signers, method);
return election.coordinatorPublicKey;
}
export function getBackupCoordinator(signers, currentCoordinatorIndex, method = ElectionMethod.LEXICOGRAPHIC) {
if (signers.length === 1) {
return null;
}
switch (method) {
case ElectionMethod.LEXICOGRAPHIC:
return getBackupForLexicographic(signers, currentCoordinatorIndex);
case ElectionMethod.HASH_BASED:
return getBackupForHashBased(signers, currentCoordinatorIndex);
case ElectionMethod.FIRST_SIGNER:
return getBackupForFirstSigner(signers, currentCoordinatorIndex);
case ElectionMethod.LAST_SIGNER:
return getBackupForLastSigner(signers, currentCoordinatorIndex);
default:
throw new Error(`Unknown election method: ${method}`);
}
}
function getBackupForLexicographic(signers, currentCoordinatorIndex) {
const indexed = signers.map((pk, idx) => ({
originalIndex: idx,
publicKey: pk,
hex: pk.toString(),
}));
indexed.sort((a, b) => a.hex.localeCompare(b.hex));
const currentSortedIndex = indexed.findIndex(item => item.originalIndex === currentCoordinatorIndex);
if (currentSortedIndex === -1) {
throw new Error('Current coordinator not found in signers list');
}
const nextSortedIndex = (currentSortedIndex + 1) % indexed.length;
return indexed[nextSortedIndex].originalIndex;
}
function getBackupForHashBased(signers, currentCoordinatorIndex) {
return (currentCoordinatorIndex + 1) % signers.length;
}
function getBackupForFirstSigner(signers, currentCoordinatorIndex) {
const nextIndex = currentCoordinatorIndex + 1;
if (nextIndex >= signers.length) {
return null;
}
return nextIndex;
}
function getBackupForLastSigner(signers, currentCoordinatorIndex) {
const nextIndex = currentCoordinatorIndex - 1;
if (nextIndex < 0) {
return null;
}
return nextIndex;
}
export function getCoordinatorPriorityList(signers, method = ElectionMethod.LEXICOGRAPHIC) {
const election = electCoordinator(signers, method);
const priorityList = [election.coordinatorIndex];
let currentIndex = election.coordinatorIndex;
while (true) {
const backup = getBackupCoordinator(signers, currentIndex, method);
if (backup === null || priorityList.includes(backup)) {
break;
}
priorityList.push(backup);
currentIndex = backup;
}
return priorityList;
}