@dashevo/dashcore-lib
Version:
A pure and powerful JavaScript Dash library.
688 lines (631 loc) • 21.4 kB
JavaScript
const groupBy = require('lodash/groupBy');
const BufferWriter = require('../encoding/bufferwriter');
const Hash = require('../crypto/hash');
const { getMerkleTree, getMerkleRoot } = require('../util/merkletree');
const SimplifiedMNListDiff = require('./SimplifiedMNListDiff');
const QuorumEntry = require('./QuorumEntry');
const SimplifiedMNListEntry = require('./SimplifiedMNListEntry');
const PartialMerkleTree = require('../block/PartialMerkleTree');
const constants = require('../constants');
const Networks = require('../networks');
const Transaction = require('../transaction');
const { MASTERNODE_TYPE_HP } = require("../constants");
/**
* @param {SimplifiedMNListDiff} simplifiedMNListDiff
* @param {Object} llmqParamsOverride
* @constructor
*/
function SimplifiedMNList(simplifiedMNListDiff = null, llmqParamsOverride = {}) {
this.baseBlockHash = constants.NULL_HASH;
this.blockHash = constants.NULL_HASH;
/**
* Note that this property contains ALL masternodes, including banned ones.
* Use getValidMasternodesList() method to get the list of only valid nodes.
* This in needed for merkleRootNMList calculation
* @type {SimplifiedMNListEntry[]}
*/
this.mnList = [];
/**
* This property contains all active quorums
* ordered by llmqType and creation time ascending.
* @type {QuorumEntry[]}
*/
this.quorumList = [];
/**
* This property contains only valid, not PoSe-banned nodes.
* @type {SimplifiedMNListEntry[]}
*/
this.validMNs = [];
this.merkleRootMNList = constants.NULL_HASH;
this.lastDiffMerkleRootMNList = constants.NULL_HASH;
this.lastDiffMerkleRootQuorums = constants.NULL_HASH;
this.quorumsActive = false;
this.cbTx = null;
this.cbTxMerkleTree = null;
this.llmqParamsOverride = llmqParamsOverride;
if (simplifiedMNListDiff) {
this.applyDiff(simplifiedMNListDiff);
}
}
/**
*
* @param {SimplifiedMNListDiff|Buffer|string|Object} simplifiedMNListDiff - serialized or parsed
* @return {void|Boolean}
*/
SimplifiedMNList.prototype.applyDiff = function applyDiff(
simplifiedMNListDiff
) {
// This will copy an instance of SimplifiedMNListDiff or create a new instance
const diff = new SimplifiedMNListDiff(
simplifiedMNListDiff,
this.network,
this.llmqParamsOverride,
);
// only when we apply the first diff we set the network
if (!this.network) {
this.network = diff.network;
}
if (this.baseBlockHash === constants.NULL_HASH) {
/* If the base block hash is a null hash, then this is the first time we apply any diff.
* If we apply diff to the list for the first time, then diff's base block hash would be
* the base block hash for the whole list.
* */
this.baseBlockHash = diff.baseBlockHash;
}
this.blockHash = diff.blockHash;
if (this.lastBlockHash && this.lastBlockHash !== diff.baseBlockHash) {
throw new Error(
"Cannot apply diff: previous blockHash needs to equal the new diff's baseBlockHash"
);
}
this.deleteMNs(diff.deletedMNs);
this.addOrUpdateMNs(diff.mnList);
this.lastDiffMerkleRootMNList = diff.merkleRootMNList || constants.NULL_HASH;
this.merkleRootMNList = this.calculateMerkleRoot();
if (this.lastDiffMerkleRootMNList !== this.merkleRootMNList) {
throw new Error(
"Merkle root from the diff doesn't match calculated merkle root after diff is applied"
);
}
this.cbTx = new Transaction(diff.cbTx);
this.cbTxMerkleTree = diff.cbTxMerkleTree.copy();
this.validMNs = this.mnList.filter((smlEntry) => smlEntry.isValid);
this.quorumsActive = this.cbTx.version >= 2;
this.nVersion = diff.nVersion;
if (this.quorumsActive) {
this.deleteQuorums(diff.deletedQuorums);
this.addAndMaybeRemoveQuorums(diff.newQuorums);
this.lastDiffMerkleRootQuorums =
diff.merkleRootQuorums || constants.NULL_HASH;
if (this.quorumList.length > 0) {
// we cannot verify the quorum merkle root for DashCore vers. < 0.16
if (this.quorumList[0].isOutdatedRPC) {
this.merkleRootQuorums = diff.merkleRootQuorums;
return;
}
this.quorumList = this.sortQuorums(this.quorumList);
this.merkleRootQuorums = this.calculateMerkleRootQuorums();
if (this.lastDiffMerkleRootQuorums !== this.merkleRootQuorums) {
throw new Error(
"merkleRootQuorums from the diff doesn't match calculated quorum root after diff is applied"
);
}
}
}
this.lastBlockHash = this.blockHash;
};
/**
* @private
* Adds MNs to the MN list
* @param {SimplifiedMNListEntry[]} mnListEntries
*/
SimplifiedMNList.prototype.addOrUpdateMNs = function addMNs(mnListEntries) {
const newMNListEntries = mnListEntries.map((mnListEntry) =>
mnListEntry.copy()
);
// eslint-disable-next-line consistent-return
newMNListEntries.forEach(function (newMNListEntry) {
const indexOfOldEntry = this.mnList.findIndex(
(oldMNListEntry) =>
oldMNListEntry.proRegTxHash === newMNListEntry.proRegTxHash
);
if (indexOfOldEntry > -1) {
this.mnList[indexOfOldEntry] = newMNListEntry;
} else {
return this.mnList.push(newMNListEntry);
}
}, this);
};
/**
* @private
* Adds quorums to the quorum list
* and maybe removes the oldest ones
* if list has reached maximum entries for llmqType
* @param {QuorumEntry[]} quorumEntries
*/
SimplifiedMNList.prototype.addAndMaybeRemoveQuorums =
function addAndMaybeRemoveQuorums(quorumEntries) {
const newGroupedQuorums = groupBy(quorumEntries, 'llmqType');
const existingQuorums = groupBy(this.quorumList, 'llmqType');
const newQuorumsTypes = Object.keys(newGroupedQuorums);
newQuorumsTypes.forEach((quorumType) => {
const numberOfQuorumsInTheList = existingQuorums[quorumType]
? existingQuorums[quorumType].length
: 0;
const numberOfQuorumsToAdd = newGroupedQuorums[quorumType]
? newGroupedQuorums[quorumType].length
: 0;
const maxAllowedQuorumsOfType = QuorumEntry.getParams(
Number(quorumType),
this.llmqParamsOverride,
).maximumActiveQuorumsCount;
if (
numberOfQuorumsInTheList + numberOfQuorumsToAdd >
maxAllowedQuorumsOfType
) {
throw new Error(
`Trying to add more quorums to quorum type ${quorumType} than its maximumActiveQuorumsCount of ${maxAllowedQuorumsOfType} permits`
);
}
this.quorumList = this.quorumList.concat(newGroupedQuorums[quorumType]);
});
};
/**
* @private
* Deletes MNs from the MN list
* @param {string[]} proRegTxHashes - list of proRegTxHashes to delete from MNList
*/
SimplifiedMNList.prototype.deleteMNs = function deleteMN(proRegTxHashes) {
proRegTxHashes.forEach(function (proRegTxHash) {
const mnIndex = this.mnList.findIndex(
(MN) => MN.proRegTxHash === proRegTxHash
);
if (mnIndex > -1) {
this.mnList.splice(mnIndex, 1);
}
}, this);
};
/**
* @private
* Deletes quorums from the quorum list
* @param {Array<obj>} deletedQuorums - deleted quorum objects
*/
SimplifiedMNList.prototype.deleteQuorums = function deleteQuorums(
deletedQuorums
) {
deletedQuorums.forEach(function (deletedQuorum) {
const quorumIndex = this.quorumList.findIndex(
(quorum) =>
quorum.llmqType === deletedQuorum.llmqType &&
quorum.quorumHash === deletedQuorum.quorumHash
);
if (quorumIndex > -1) {
this.quorumList.splice(quorumIndex, 1);
}
}, this);
};
/**
* Compares merkle root from the most recent diff applied matches the merkle root of the list
* @returns {boolean}
*/
SimplifiedMNList.prototype.verify = function verify() {
return this.calculateMerkleRoot() === this.lastDiffMerkleRootMNList;
};
/**
* @private
* Sorts MN List in deterministic order
*/
SimplifiedMNList.prototype.sort = function sort() {
this.mnList.sort((a, b) =>
Buffer.compare(
Buffer.from(a.proRegTxHash, 'hex').reverse(),
Buffer.from(b.proRegTxHash, 'hex').reverse()
)
);
};
/**
* @private
* @param {QuorumEntry[]} quorumList - sort array of quorum entries
* Sorts the quorums deterministically
*/
SimplifiedMNList.prototype.sortQuorums = function sortQuorumsEntries(
quorumList
) {
quorumList.sort((a, b) => {
const hashA = Buffer.from(a.calculateHash()).reverse();
const hashB = Buffer.from(b.calculateHash()).reverse();
return Buffer.compare(hashA, hashB);
});
return quorumList;
};
/**
* Calculates merkle root of the MN list
* @returns {string}
*/
SimplifiedMNList.prototype.calculateMerkleRoot =
function calculateMerkleRoot() {
if (this.mnList.length < 1) {
return constants.NULL_HASH;
}
this.sort();
const sortedEntryHashes = this.mnList.map((mnListEntry) =>
mnListEntry.calculateHash()
);
return getMerkleRoot(getMerkleTree(sortedEntryHashes))
.reverse()
.toString('hex');
};
/**
* Calculates merkle root of the quorum list
* @returns {string}
*/
SimplifiedMNList.prototype.calculateMerkleRootQuorums =
function calculateMerkleRootQuorums() {
if (this.quorumList.length < 1) {
return constants.NULL_HASH;
}
const sortedHashes = this.quorumList.map((quorum) =>
quorum.calculateHash().reverse()
);
return getMerkleRoot(getMerkleTree(sortedHashes)).reverse().toString('hex');
};
/**
* Returns a list of valid masternodes
* @returns {SimplifiedMNListEntry[]}
*/
SimplifiedMNList.prototype.getValidMasternodesList =
function getValidMasternodes() {
return this.validMNs;
};
/**
* Returns a single quorum
* @param {constants.LLMQ_TYPES} llmqType - llmqType of quorum
* @param {string} quorumHash - quorumHash of quorum
* @returns {QuorumEntry}
*/
SimplifiedMNList.prototype.getQuorum = function getQuorum(
llmqType,
quorumHash
) {
return this.quorumList.find(
(quorum) => quorum.llmqType === llmqType && quorum.quorumHash === quorumHash
);
};
/**
* Returns all quorums - verified or unverified
* @returns {QuorumEntry[]}
*/
SimplifiedMNList.prototype.getQuorums = function getQuorums() {
return this.quorumList;
};
/**
* Returns only quorums of type llmqType - verified or unverified
* @param {constants.LLMQ_TYPES} llmqType - llmqType of quorum
* @returns {QuorumEntry[]}
*/
SimplifiedMNList.prototype.getQuorumsOfType = function getQuorumsOfType(
llmqType
) {
return this.quorumList.filter((quorum) => quorum.llmqType === llmqType);
};
/**
* Returns all already verified quorums
* @returns {QuorumEntry[]}
*/
SimplifiedMNList.prototype.getVerifiedQuorums = function getVerifiedQuorums() {
return this.quorumList.filter((quorum) => quorum.isVerified);
};
/**
* Returns only already verified quorums of type llmqType
* @param {constants.LLMQ_TYPES} llmqType - llmqType of quorum
* @returns {QuorumEntry[]}
*/
SimplifiedMNList.prototype.getVerifiedQuorumsOfType =
function getVerifiedQuorumsOfType(llmqType) {
return this.quorumList.filter(
(quorum) => quorum.isVerified && quorum.llmqType === llmqType
);
};
/**
* Returns all still unverified quorums
* @returns {QuorumEntry[]}
*/
SimplifiedMNList.prototype.getUnverifiedQuorums =
function getUnverifiedQuorums() {
return this.quorumList.filter((quorum) => !quorum.isVerified);
};
/**
* @return {constants.LLMQ_TYPES[]}
*/
SimplifiedMNList.prototype.getLLMQTypes = function getLLMQTypes() {
let llmqTypes = [];
if (!this.network) {
throw new Error('Network is not set');
}
switch (this.network.name) {
case Networks.livenet.name:
llmqTypes = [
constants.LLMQ_TYPES.LLMQ_TYPE_50_60,
constants.LLMQ_TYPES.LLMQ_TYPE_60_75,
constants.LLMQ_TYPES.LLMQ_TYPE_400_60,
constants.LLMQ_TYPES.LLMQ_TYPE_400_85,
constants.LLMQ_TYPES.LLMQ_TYPE_100_67,
];
return llmqTypes;
case Networks.testnet.name:
if (this.mnList.length > 100) {
llmqTypes = [
constants.LLMQ_TYPES.LLMQ_TYPE_50_60,
constants.LLMQ_TYPES.LLMQ_TYPE_60_75,
constants.LLMQ_TYPES.LLMQ_TYPE_400_60,
constants.LLMQ_TYPES.LLMQ_TYPE_400_85,
constants.LLMQ_TYPES.LLMQ_TYPE_TEST_V17,
constants.LLMQ_TYPES.LLMQ_TYPE_25_67,
];
return llmqTypes;
}
// regtest
if (Networks.testnet.regtestEnabled === true) {
llmqTypes = [
constants.LLMQ_TYPES.LLMQ_TYPE_LLMQ_TEST,
constants.LLMQ_TYPES.LLMQ_TYPE_50_60,
constants.LLMQ_TYPES.LLMQ_TYPE_60_75,
constants.LLMQ_TYPES.LLMQ_TYPE_TEST_V17,
constants.LLMQ_TYPES.LLMQ_TYPE_TEST_INSTANTSEND,
constants.LLMQ_TYPES.LLMQ_TYPE_TEST_DIP0024,
constants.LLMQ_TYPES.LLMQ_TEST_PLATFORM,
];
return llmqTypes;
}
// devnet
llmqTypes = [
constants.LLMQ_TYPES.LLMQ_TYPE_LLMQ_DEVNET,
constants.LLMQ_TYPES.LLMQ_TYPE_50_60,
constants.LLMQ_TYPES.LLMQ_TYPE_60_75,
constants.LLMQ_TYPES.LLMQ_TYPE_400_60,
constants.LLMQ_TYPES.LLMQ_TYPE_400_85,
constants.LLMQ_TYPES.LLMQ_TYPE_TEST_V17,
constants.LLMQ_TYPES.LLMQ_TYPE_TEST_INSTANTSEND,
constants.LLMQ_TYPES.LLMQ_TYPE_TEST_DIP0024,
constants.LLMQ_TYPES.LLMQ_DEVNET_DIP0024,
constants.LLMQ_TYPES.LLMQ_DEVNET_PLATFORM,
];
return llmqTypes;
default:
throw new Error('Unknown network');
}
};
/**
* @return {constants.LLMQ_TYPES}
*/
SimplifiedMNList.prototype.getPlatformLLMQType =
function getPlatformLLMQType() {
if (!this.network) {
throw new Error('Network is not set');
}
switch (this.network.name) {
case Networks.livenet.name:
return constants.LLMQ_TYPES.LLMQ_TYPE_100_67;
case Networks.testnet.name:
if (this.mnList.length > 100) {
return constants.LLMQ_TYPES.LLMQ_TYPE_25_67;
}
// regtest
if (Networks.testnet.regtestEnabled === true) {
return constants.LLMQ_TYPES.LLMQ_TEST_PLATFORM;
}
// devnet
return constants.LLMQ_TYPES.LLMQ_DEVNET_PLATFORM;
default:
throw new Error('Unknown network');
}
};
/**
* @return {constants.LLMQ_TYPES}
*/
SimplifiedMNList.prototype.getChainlockLLMQType =
function getChainlockLLMQType() {
if (!this.network) {
throw new Error('Network is not set');
}
switch (this.network.name) {
case Networks.livenet.name:
return constants.LLMQ_TYPES.LLMQ_TYPE_400_60;
case Networks.testnet.name:
if (this.mnList.length > 100) {
return constants.LLMQ_TYPES.LLMQ_TYPE_50_60;
}
// regtest
if (Networks.testnet.regtestEnabled === true) {
return constants.LLMQ_TYPES.LLMQ_TYPE_LLMQ_TEST;
}
// devnet
return constants.LLMQ_TYPES.LLMQ_TYPE_50_60;
default:
throw new Error('Unknown network');
}
};
/**
* @return {constants.LLMQ_TYPES}
*/
SimplifiedMNList.prototype.getValidatorLLMQType =
function getValidatorLLMQType() {
if (!this.network) {
throw new Error('Network is not set');
}
switch (this.network.name) {
case Networks.livenet.name:
return constants.LLMQ_TYPES.LLMQ_TYPE_100_67;
case Networks.testnet.name:
if (this.mnList.length > 100) {
return constants.LLMQ_TYPES.LLMQ_TYPE_100_67;
}
// regtest
if (Networks.testnet.regtestEnabled === true) {
return constants.LLMQ_TYPES.LLMQ_TYPE_LLMQ_TEST;
}
// devnet
return constants.LLMQ_TYPES.LLMQ_TYPE_LLMQ_DEVNET;
default:
throw new Error('Unknown network');
}
};
/**
* @return {constants.LLMQ_TYPES}
*/
SimplifiedMNList.prototype.getInstantSendLLMQType =
function getInstantSendLLMQType() {
if (!this.network) {
throw new Error('Network is not set');
}
switch (this.network.name) {
case Networks.livenet.name:
return constants.LLMQ_TYPES.LLMQ_TYPE_25_67;
case Networks.testnet.name:
if (this.mnList.length > 100) {
return constants.LLMQ_TYPES.LLMQ_TYPE_25_67;
}
// regtest
if (Networks.testnet.regtestEnabled === true) {
return constants.LLMQ_TYPES.LLMQ_TYPE_TEST_INSTANTSEND;
}
// devnet
return constants.LLMQ_TYPES.LLMQ_DEVNET_DIP0024;
default:
throw new Error('Unknown network');
}
};
/**
*
* @param {constants.LLMQ_TYPES} llmq
*/
SimplifiedMNList.prototype.isLLMQTypeRotated =
function isLLMQTypeRotated(llmq) {
return llmq === constants.LLMQ_TYPES.LLMQ_DEVNET_DIP0024 ||
llmq === constants.LLMQ_TYPES.LLMQ_TYPE_TEST_DIP0024 ||
llmq === constants.LLMQ_TYPES.LLMQ_TYPE_60_75;
};
/**
* Converts simplified MN list to simplified MN list diff that can be used to serialize data
* to json, buffer, or a hex string
* @param {string} [network]
*/
SimplifiedMNList.prototype.toSimplifiedMNListDiff =
function toSimplifiedMNListDiff(network) {
if (!this.cbTx || !this.cbTxMerkleTree) {
throw new Error("Can't convert MN list to diff - cbTx is missing");
}
return SimplifiedMNListDiff.fromObject(
{
baseBlockHash: this.baseBlockHash,
blockHash: this.blockHash,
cbTx: new Transaction(this.cbTx),
cbTxMerkleTree: this.cbTxMerkleTree,
// Always empty, as simplified MN list doesn't have a deleted mn list
deletedMNs: [],
mnList: this.mnList,
deletedQuorums: [],
newQuorums: this.quorumList,
merkleRootMNList: this.merkleRootMNList,
merkleRootQuorums: this.merkleRootQuorums,
nVersion: this.nVersion,
},
network,
this.llmqParamsOverride,
);
};
/**
* Recreates SML from json
* @param {Object} smlJson
* @param {Object} llmqParamsOverride
*/
SimplifiedMNList.fromJSON = function fromJSON(smlJson, llmqParamsOverride = {}) {
const sml = new SimplifiedMNList();
sml.baseBlockHash = smlJson.baseBlockHash;
sml.blockHash = smlJson.blockHash;
sml.merkleRootMNList = smlJson.merkleRootMNList;
sml.lastDiffMerkleRootMNList = smlJson.lastDiffMerkleRootMNList;
sml.lastDiffMerkleRootQuorums = smlJson.lastDiffMerkleRootQuorums;
sml.quorumsActive = smlJson.quorumsActive;
sml.cbTx = new Transaction(smlJson.cbTx);
sml.cbTxMerkleTree = new PartialMerkleTree();
sml.cbTxMerkleTree.totalTransactions =
smlJson.cbTxMerkleTree.totalTransactions;
sml.cbTxMerkleTree.merkleHashes = smlJson.cbTxMerkleTree.merkleHashes;
sml.cbTxMerkleTree.merkleFlags = smlJson.cbTxMerkleTree.merkleFlags;
sml.mnList = smlJson.mnList.map(
(mnRecord) => new SimplifiedMNListEntry(mnRecord)
);
sml.quorumList = smlJson.quorumList.map(
(quorumEntry) => new QuorumEntry(quorumEntry, llmqParamsOverride),
);
sml.validMNs = smlJson.validMNs.map(
(mnRecord) => new SimplifiedMNListEntry(mnRecord)
);
sml.nVersion = smlJson.nVersion;
return sml;
};
/**
* Deterministically selects all members of the quorum which
* has started it's DKG session with the block of this MNList
* @param {Buffer} selectionModifier
* @param {number} size
* @param {boolean} onlyHighPerformanceMasternodes
* @return {SimplifiedMNListEntry[]}
*/
SimplifiedMNList.prototype.calculateQuorum = function calculateQuorum(
selectionModifier,
size,
onlyHighPerformanceMasternodes,
) {
const scores = this.calculateScores(selectionModifier, onlyHighPerformanceMasternodes);
scores.sort((a, b) => Buffer.compare(a.score, b.score));
scores.reverse();
return scores.map((score) => score.mn).slice(0, size);
};
/**
* Calculates scores for MN selection
* it calculates sha256(sha256(proTxHash, confirmedHash), modifier) per MN
* Please note that this is not a double-sha256 but a single-sha256
* @param {Buffer} modifier
* @param {boolean} onlyHighPerformanceMasternodes
* @return {Object[]} scores
*/
SimplifiedMNList.prototype.calculateScores = function calculateScores(
modifier,
onlyHighPerformanceMasternodes
) {
return this.validMNs
.filter((mn) => mn.confirmedHash !== constants.NULL_HASH)
.filter((mn) => onlyHighPerformanceMasternodes ? mn.nType === MASTERNODE_TYPE_HP : true)
.map((mn) => {
const bufferWriter = new BufferWriter();
bufferWriter.writeReverse(mn.confirmedHashWithProRegTxHash());
bufferWriter.writeReverse(modifier);
return { score: Hash.sha256(bufferWriter.toBuffer()).reverse(), mn };
});
};
/**
* Calculates scores for quorum signing selection
* it calculates sha256(sha256(proTxHash, confirmedHash), modifier) per MN
* Please note that this is not a double-sha256 but a single-sha256
* @param {constants.LLMQ_TYPES} llmqType
* @param {Buffer} modifier
* @return {Object[]} scores
*/
SimplifiedMNList.prototype.calculateSignatoryQuorumScores =
function calculateSignatoryQuorumScores(llmqType, modifier) {
// for now we don't care if quorums have been verified or not
return this.getQuorumsOfType(llmqType).map((quorum, index) => {
const bufferWriter = new BufferWriter();
bufferWriter.writeUInt8(llmqType);
bufferWriter.writeReverse(Buffer.from(quorum.quorumHash, 'hex'));
bufferWriter.writeReverse(modifier);
return {
score: Hash.sha256sha256(bufferWriter.toBuffer()),
index,
quorum,
};
});
};
module.exports = SimplifiedMNList;