UNPKG

alterdot-lib

Version:

A pure and powerful JavaScript Alterdot library.

530 lines (487 loc) 16.5 kB
const _ = require('lodash'); const $ = require('../util/preconditions'); const BitArray = require('../util/bitarray'); const BufferReader = require('../encoding/bufferreader'); const BufferWriter = require('../encoding/bufferwriter'); const BufferUtil = require('../util/buffer'); const constants = require('../constants'); const Hash = require('../crypto/hash'); const bls = require('../crypto/bls'); const utils = require('../util/js'); const { isHexStringOfSize, isUnsignedInteger, isSha256HexString: isSha256, isHexaString: isHexString, } = utils; const { BLS_PUBLIC_KEY_SIZE, BLS_SIGNATURE_SIZE, SHA256_HASH_SIZE } = constants; /** * @typedef {Object} SMLQuorumEntry * @property {number} version * @property {number} llmqType * @property {string} quorumHash * @property {number} signersCount * @property {string} signers * @property {number} validMembersCount * @property {string} validMembers * @property {string} quorumPublicKey * @property {string} quorumVvecHash * @property {string} quorumSig * @property {string} membersSig */ /** * @class QuorumEntry * @param {string|Object|Buffer} [arg] - A Buffer, JSON string, * or Object representing a SMLQuorumEntry * @constructor * @property {number} version * @property {number} llmqType * @property {string} quorumHash * @property {number} signersCount * @property {string} signers * @property {number} validMembersCount * @property {string} validMembers * @property {string} quorumPublicKey * @property {string} quorumVvecHash * @property {string} quorumSig * @property {string} membersSig */ function QuorumEntry(arg) { if (arg) { if (arg instanceof QuorumEntry) { return arg.copy(); } if (BufferUtil.isBuffer(arg)) { return QuorumEntry.fromBuffer(arg); } if (_.isObject(arg)) { return QuorumEntry.fromObject(arg); } if (arg instanceof QuorumEntry) { return arg.copy(); } if (isHexString(arg)) { return QuorumEntry.fromHexString(arg); } throw new TypeError('Unrecognized argument for QuorumEntry'); } } /** * Parse buffer and returns QuorumEntry * @param {Buffer} buffer * @return {QuorumEntry} */ QuorumEntry.fromBuffer = function fromBuffer(buffer) { const bufferReader = new BufferReader(buffer); const SMLQuorumEntry = new QuorumEntry(); SMLQuorumEntry.isVerified = false; if (buffer.length < 100) { SMLQuorumEntry.isOutdatedRPC = true; SMLQuorumEntry.version = bufferReader.readUInt16LE(); SMLQuorumEntry.llmqType = bufferReader.readUInt8(); SMLQuorumEntry.quorumHash = bufferReader .read(constants.SHA256_HASH_SIZE) .reverse() .toString('hex'); SMLQuorumEntry.signersCount = bufferReader.readVarintNum(); SMLQuorumEntry.validMembersCount = bufferReader.readVarintNum(); SMLQuorumEntry.quorumPublicKey = bufferReader .read(BLS_PUBLIC_KEY_SIZE) .toString('hex'); return SMLQuorumEntry; } SMLQuorumEntry.isOutdatedRPC = false; SMLQuorumEntry.version = bufferReader.readUInt16LE(); SMLQuorumEntry.llmqType = bufferReader.readUInt8(); SMLQuorumEntry.quorumHash = bufferReader .read(constants.SHA256_HASH_SIZE) .reverse() .toString('hex'); SMLQuorumEntry.signersCount = bufferReader.readVarintNum(); const signersBytesToRead = Math.floor((SMLQuorumEntry.getParams().size + 7) / 8) || 1; SMLQuorumEntry.signers = bufferReader .read(signersBytesToRead) .toString('hex'); SMLQuorumEntry.validMembersCount = bufferReader.readVarintNum(); const validMembersBytesToRead = Math.floor((SMLQuorumEntry.getParams().size + 7) / 8) || 1; SMLQuorumEntry.validMembers = bufferReader .read(validMembersBytesToRead) .toString('hex'); SMLQuorumEntry.quorumPublicKey = bufferReader .read(BLS_PUBLIC_KEY_SIZE) .toString('hex'); SMLQuorumEntry.quorumVvecHash = bufferReader .read(SHA256_HASH_SIZE) .reverse() .toString('hex'); SMLQuorumEntry.quorumSig = bufferReader .read(BLS_SIGNATURE_SIZE) .toString('hex'); SMLQuorumEntry.membersSig = bufferReader .read(BLS_SIGNATURE_SIZE) .toString('hex'); return SMLQuorumEntry; }; /** * @param {string} string * @return {QuorumEntry} */ QuorumEntry.fromHexString = function fromString(string) { return QuorumEntry.fromBuffer(Buffer.from(string, 'hex')); }; /** * Serialize SML entry to buf * @return {Buffer} */ QuorumEntry.prototype.toBuffer = function toBuffer() { this.validate(); const bufferWriter = new BufferWriter(); bufferWriter.writeUInt16LE(this.version); bufferWriter.writeUInt8(this.llmqType); bufferWriter.write(Buffer.from(this.quorumHash, 'hex').reverse()); bufferWriter.writeVarintNum(this.signersCount); if (this.isOutdatedRPC) { bufferWriter.writeVarintNum(this.validMembersCount); bufferWriter.write(Buffer.from(this.quorumPublicKey, 'hex')); return bufferWriter.toBuffer(); } bufferWriter.write(Buffer.from(this.signers, 'hex')); bufferWriter.writeVarintNum(this.validMembersCount); bufferWriter.write(Buffer.from(this.validMembers, 'hex')); bufferWriter.write(Buffer.from(this.quorumPublicKey, 'hex')); bufferWriter.write(Buffer.from(this.quorumVvecHash, 'hex').reverse()); bufferWriter.write(Buffer.from(this.quorumSig, 'hex')); bufferWriter.write(Buffer.from(this.membersSig, 'hex')); return bufferWriter.toBuffer(); }; /** * Serialize SML entry to buf * @return {Buffer} */ QuorumEntry.prototype.toBufferForHashing = function toBufferForHashing() { this.validate(); const bufferWriter = new BufferWriter(); const fixedCounterLength = this.getParams().size; bufferWriter.writeUInt16LE(this.version); bufferWriter.writeUInt8(this.llmqType); bufferWriter.write(Buffer.from(this.quorumHash, 'hex').reverse()); bufferWriter.writeVarintNum(fixedCounterLength); bufferWriter.write(Buffer.from(this.signers, 'hex')); bufferWriter.writeVarintNum(fixedCounterLength); bufferWriter.write(Buffer.from(this.validMembers, 'hex')); bufferWriter.write(Buffer.from(this.quorumPublicKey, 'hex')); bufferWriter.write(Buffer.from(this.quorumVvecHash, 'hex').reverse()); bufferWriter.write(Buffer.from(this.quorumSig, 'hex')); bufferWriter.write(Buffer.from(this.membersSig, 'hex')); return bufferWriter.toBuffer(); }; /** * Create SMLQuorumEntry from an object * @param {SMLQuorumEntry} obj * @return {QuorumEntry} */ QuorumEntry.fromObject = function fromObject(obj) { const SMLQuorumEntry = new QuorumEntry(); SMLQuorumEntry.isVerified = false; SMLQuorumEntry.isOutdatedRPC = false; SMLQuorumEntry.version = obj.version; SMLQuorumEntry.llmqType = obj.llmqType; SMLQuorumEntry.quorumHash = obj.quorumHash; SMLQuorumEntry.signersCount = obj.signersCount; SMLQuorumEntry.signers = obj.signers; SMLQuorumEntry.validMembersCount = obj.validMembersCount; SMLQuorumEntry.validMembers = obj.validMembers; SMLQuorumEntry.quorumPublicKey = obj.quorumPublicKey; SMLQuorumEntry.quorumVvecHash = obj.quorumVvecHash; SMLQuorumEntry.quorumSig = obj.quorumSig; SMLQuorumEntry.membersSig = obj.membersSig; if (SMLQuorumEntry.signers === undefined) { SMLQuorumEntry.isOutdatedRPC = true; } SMLQuorumEntry.validate(); return SMLQuorumEntry; }; QuorumEntry.prototype.validate = function validate() { $.checkArgument( utils.isUnsignedInteger(this.version), 'Expect version to be an unsigned integer' ); $.checkArgument( utils.isUnsignedInteger(this.llmqType), 'Expect llmqType to be an unsigned integer' ); $.checkArgument( isSha256(this.quorumHash), 'Expected quorumHash to be a sha256 hex string' ); $.checkArgument( isUnsignedInteger(this.signersCount), 'Expect signersCount to be an unsigned integer' ); $.checkArgument( isUnsignedInteger(this.validMembersCount), 'Expect validMembersCount to be an unsigned integer' ); $.checkArgument( isHexStringOfSize(this.quorumPublicKey, BLS_PUBLIC_KEY_SIZE * 2), 'Expected quorumPublicKey to be a bls pubkey' ); if (!this.isOutdatedRPC) { $.checkArgument( utils.isHexaString(this.signers), 'Expect signers to be a hex string' ); $.checkArgument( utils.isHexaString(this.validMembers), 'Expect validMembers to be a hex string' ); $.checkArgument( isHexStringOfSize(this.quorumVvecHash, SHA256_HASH_SIZE * 2), `Expected quorumVvecHash to be a hex string of size ${SHA256_HASH_SIZE}` ); $.checkArgument( isHexStringOfSize(this.quorumSig, BLS_SIGNATURE_SIZE * 2), 'Expected quorumSig to be a bls signature' ); $.checkArgument( isHexStringOfSize(this.membersSig, BLS_SIGNATURE_SIZE * 2), 'Expected membersSig to be a bls signature' ); } }; QuorumEntry.prototype.toObject = function toObject() { return { version: this.version, llmqType: this.llmqType, quorumHash: this.quorumHash, signersCount: this.signersCount, signers: this.signers, validMembersCount: this.validMembersCount, validMembers: this.validMembers, quorumPublicKey: this.quorumPublicKey, quorumVvecHash: this.quorumVvecHash, quorumSig: this.quorumSig, membersSig: this.membersSig, }; }; QuorumEntry.getParams = function getParams(llmqType) { const params = {}; switch (llmqType) { case constants.LLMQ_TYPES.LLMQ_TYPE_50_60: params.size = 50; params.threshold = 30; params.maximumActiveQuorumsCount = 24; return params; case constants.LLMQ_TYPES.LLMQ_TYPE_400_60: params.size = 400; params.threshold = 240; params.maximumActiveQuorumsCount = 4; return params; case constants.LLMQ_TYPES.LLMQ_TYPE_400_85: params.size = 400; params.threshold = 340; params.maximumActiveQuorumsCount = 4; return params; case constants.LLMQ_TYPES.LLMQ_TYPE_100_67: params.size = 100; params.threshold = 67; params.maximumActiveQuorumsCount = 24; return params; case constants.LLMQ_TYPES.LLMQ_TYPE_LLMQ_TEST: params.size = 3; params.threshold = 2; params.maximumActiveQuorumsCount = 2; return params; case constants.LLMQ_TYPES.LLMQ_TYPE_LLMQ_DEVNET: params.size = 10; params.threshold = 3; params.maximumActiveQuorumsCount = 7; return params; case constants.LLMQ_TYPES.LLMQ_TYPE_TEST_V17: params.size = 3; params.threshold = 2; params.maximumActiveQuorumsCount = 2; return params; case constants.LLMQ_TYPES.LLMQ_TYPE_10_60: params.size = 10; params.threshold = 6; params.maximumActiveQuorumsCount = 4; return params; case constants.LLMQ_TYPES.LLMQ_TYPE_30_80: params.size = 30; params.threshold = 24; params.maximumActiveQuorumsCount = 2; return params; default: throw new Error(`Invalid llmq type ${llmqType}`); } }; /** * @return {number} */ QuorumEntry.prototype.getParams = function getParams() { return QuorumEntry.getParams(this.llmqType); }; /** * Serialize quorum entry commitment to buf * This is the message hash signed by the quorum for verification * @return {Uint8Array} */ QuorumEntry.prototype.getCommitmentHash = function getCommitmentHash() { const bufferWriter = new BufferWriter(); bufferWriter.writeUInt8(this.llmqType); bufferWriter.write(Buffer.from(this.quorumHash, 'hex').reverse()); bufferWriter.writeVarintNum(this.getParams().size); bufferWriter.write(Buffer.from(this.validMembers, 'hex')); bufferWriter.write(Buffer.from(this.quorumPublicKey, 'hex')); bufferWriter.write(Buffer.from(this.quorumVvecHash, 'hex').reverse()); return Hash.sha256sha256(bufferWriter.toBuffer()); }; /** * Verifies the quorum's bls threshold signature * @return {Promise<boolean>} */ QuorumEntry.prototype.isValidQuorumSig = async function isValidQuorumSig() { if (this.isOutdatedRPC) { throw new Error( 'Quorum cannot be verified: node running on outdated AlterdotCore version (< 0.16)' ); } return bls.verifySignature( this.quorumSig, Uint8Array.from(this.getCommitmentHash()), this.quorumPublicKey ); }; /** * Verifies the quorum's aggregated operator key signature * @param {SimplifiedMNList} mnList - MNList for the block (quorumHash) * @return {Promise<boolean>} */ QuorumEntry.prototype.isValidMemberSig = async function isValidMemberSig( mnList ) { if (mnList.blockHash !== this.quorumHash) { throw new Error(`Wrong Masternode List for quorum: blockHash ${mnList.blockHash} doesn't correspond with quorumHash ${this.quorumHash}`); } if (this.isOutdatedRPC) { throw new Error( 'Quorum cannot be verified: node running on outdated AlterdotCore version (< 0.16)' ); } const quorumMembers = this.getAllQuorumMembers(mnList); const publicKeyStrings = quorumMembers.map( (quorumMember) => quorumMember.pubKeyOperator ); const signersBits = BitArray.uint8ArrayToBitArray( Uint8Array.from(Buffer.from(this.signers, 'hex')) ); return bls.verifyAggregatedSignature( this.membersSig, Uint8Array.from(this.getCommitmentHash()), publicKeyStrings, signersBits ); }; /** * verifies the quorum against the det. MNList that was active * when the quorum was starting its DKG session. Two different * types of BLS signature verifications are performed: * 1. the quorumSig is verified with the quorumPublicKey * 2. the quorum members are re-calculated and the memberSig is * verified against their aggregated pubKeyOperator values * @param {SimplifiedMNList} quorumSMNList - MNList for the block (quorumHash) * the quorum was starting its DKG session with * @return {Promise<boolean>} */ QuorumEntry.prototype.verify = function verify(quorumSMNList) { return new Promise((resolve, reject) => { if (quorumSMNList.blockHash !== this.quorumHash) { return reject( new Error(`Wrong Masternode List for quorum: blockHash ${quorumSMNList.blockHash} doesn't correspond with quorumHash ${this.quorumHash}`) ); } if (this.isOutdatedRPC) { return reject( new Error( 'Quorum cannot be verified: node running on outdated AlterdotCore version (< 0.16)' ) ); } // only verify if quorum hasn't already been verified if (this.isVerified) { return resolve(true); } return this.isValidMemberSig(quorumSMNList) .then((isValidMemberSig) => { if (!isValidMemberSig) { return false; } return this.isValidQuorumSig(); }) .then((isVerified) => { this.isVerified = isVerified; resolve(isVerified); }); }); }; /** * Get all members for this quorum * @param {SimplifiedMNList} SMNList - MNlist for the quorum * @return {SimplifiedMNListEntry[]} */ QuorumEntry.prototype.getAllQuorumMembers = function getAllQuorumMembers( SMNList ) { if (SMNList.blockHash !== this.quorumHash) { throw new Error(`Wrong Masternode List for quorum: blockHash ${SMNList.blockHash} doesn't correspond with quorumHash ${this.quorumHash}`); } return SMNList.calculateQuorum( this.getSelectionModifier(), this.getParams().size ); }; /** * Gets the modifier for deterministic sorting of the MNList * for quorum member selection * @return {Buffer} */ QuorumEntry.prototype.getSelectionModifier = function getSelectionModifier() { const bufferWriter = new BufferWriter(); bufferWriter.writeUInt8(this.llmqType); bufferWriter.write(Buffer.from(this.quorumHash, 'hex').reverse()); return Hash.sha256sha256(bufferWriter.toBuffer()).reverse(); }; /** * Gets the ordering hash for a requestId * @param {string} requestId - the requestId for the signing session to be verified * @return {Buffer} */ QuorumEntry.prototype.getOrderingHashForRequestId = function getOrderingHashForRequestId(requestId) { const buf = Buffer.concat([ Buffer.from(this.llmqType), Buffer.from(this.quorumHash, 'hex'), Buffer.from(requestId, 'hex'), ]); return Hash.sha256sha256(buf).reverse(); }; /** * @return {Buffer} */ QuorumEntry.prototype.calculateHash = function calculateHash() { return Hash.sha256sha256(this.toBufferForHashing()).reverse(); }; /** * Creates a copy of QuorumEntry * @return {QuorumEntry} */ QuorumEntry.prototype.copy = function copy() { return QuorumEntry.fromBuffer(this.toBuffer()); }; module.exports = QuorumEntry;