diff --git a/.gitignore b/.gitignore index 3484fcd91e..78f95e223b 100644 --- a/.gitignore +++ b/.gitignore @@ -117,4 +117,4 @@ data* .vscode/launch.json # KAs Distribution Simulation Script Plots -tools/knowledge-assets-distribution-simulation/plots/*jpg +tools/knowledge-assets-distribution-simulation/plots/**/*jpg diff --git a/src/service/proximity-scoring-service.js b/src/service/proximity-scoring-service.js index 1cfd4fd439..37a74c82fc 100644 --- a/src/service/proximity-scoring-service.js +++ b/src/service/proximity-scoring-service.js @@ -11,6 +11,13 @@ class ProximityScoringService { this.proximityScoreFunctionsPairs = { 1: [this.calculateBinaryXOR.bind(this), this.Log2PLDSF.bind(this)], 2: [this.calculateProximityOnHashRing.bind(this), this.LinearLogisticSum.bind(this)], + 3: [this.calculateProximityOnHashRing.bind(this), this.LinearSum.bind(this)], + 4: [this.calculateProximityOnHashRing.bind(this), this.LinearDivision.bind(this)], + 5: [ + this.calculateProximityOnHashRing.bind(this), + this.LinearEMANormalization.bind(this), + ], + 6: [this.calculateRelativeDistance.bind(this), this.RelativeNormalization.bind(this)], }; } @@ -62,17 +69,55 @@ class ProximityScoringService { keyHash, ); - let directDistance = peerPositionOnHashRing.sub(keyPositionOnHashRing); - - if (directDistance.lt(0)) { - directDistance = directDistance.add(HASH_RING_SIZE); - } - + const directDistance = peerPositionOnHashRing.gt(keyPositionOnHashRing) + ? peerPositionOnHashRing.sub(keyPositionOnHashRing) + : keyPositionOnHashRing.sub(peerPositionOnHashRing); const wraparoundDistance = HASH_RING_SIZE.sub(directDistance); return directDistance.lt(wraparoundDistance) ? directDistance : wraparoundDistance; } + async calculateRelativeDistance(blockchain, peerHash, keyHash, nodes) { + const peerHashBN = await this.blockchainModuleManager.toBigNumber(blockchain, peerHash); + const keyHashBN = await this.blockchainModuleManager.toBigNumber(blockchain, keyHash); + + const positions = await Promise.all( + nodes.map(async (node) => + this.blockchainModuleManager.toBigNumber(blockchain, node.sha256), + ), + ); + + const closestNode = positions.reduce((prev, curr) => { + const diffCurr = curr.sub(keyHashBN).abs(); + const diffPrev = prev.sub(keyHashBN).abs(); + + return diffCurr.lt(diffPrev) ? curr : prev; + }); + + const sortedPositions = positions.sort((a, b) => a.sub(b)); + + const closestNodeIndex = sortedPositions.findIndex((pos) => pos.eq(closestNode)); + const peerIndex = sortedPositions.findIndex((pos) => pos.eq(peerHashBN)); + + return this.blockchainModuleManager.toBigNumber( + blockchain, + Math.abs(peerIndex - closestNodeIndex), + ); + } + + async RelativeNormalization(blockchain, distance, stake) { + const w1 = 3; + const w2 = 1; + + const mappedDistance = distance / 10; + const proximityScore = 1 - mappedDistance; + const mappedStake = (stake - 50000) / (1000000 - 50000); + + const score = w1 * proximityScore + w2 * mappedStake; + + return { mappedDistance, mappedStake, score }; + } + async Log2PLDSF(blockchain, distance, stake) { const log2PLDSFParams = await this.blockchainModuleManager.getLog2PLDSFParams(blockchain); @@ -98,10 +143,16 @@ class ProximityScoringService { const dividend = mappedStake.pow(stakeExponent).mul(a).add(b); const divisor = mappedDistance.pow(distanceExponent).mul(c).add(d); - return Math.floor( - Number(multiplier) * - Math.log2(Number(logArgumentConstant) + dividend.toNumber() / divisor.toNumber()), - ); + return { + mappedDistance, + mappedStake, + score: Math.floor( + Number(multiplier) * + Math.log2( + Number(logArgumentConstant) + dividend.toNumber() / divisor.toNumber(), + ), + ), + }; } // Using Maclaurin Series to approximate e^x @@ -142,7 +193,90 @@ class ProximityScoringService { const proximityScore = w1 * (1 - mappedDistance); const stakeScore = w2 * mappedStake; - return proximityScore + stakeScore; + return { mappedDistance, mappedStake, score: proximityScore + stakeScore }; + } + + async LinearSum(blockchain, distance, stake) { + const linearSumParams = await this.blockchainModuleManager.getLinearSumParams(blockchain); + const { distanceScaleFactor, w1, w2 } = linearSumParams; + + let dividend = distance; + let divisor = HASH_RING_SIZE.div(2); + if (dividend.gt(UINT128_MAX_BN) || divisor.gt(UINT128_MAX_BN)) { + dividend = dividend.div(distanceScaleFactor); + divisor = divisor.div(distanceScaleFactor); + } + + const divResult = dividend.mul(distanceScaleFactor).div(divisor); + const mappedDistance = + parseFloat(divResult.toString()) / parseFloat(distanceScaleFactor.toString()); + + const maxStake = await this.blockchainModuleManager.getMaximumStake(blockchain); + const mappedStake = + stake / Number(await this.blockchainModuleManager.convertFromWei(maxStake)); + + const proximityScore = w1 * (1 - mappedDistance); + const stakeScore = w2 * mappedStake; + + return { mappedDistance, mappedStake, score: proximityScore + stakeScore }; + } + + async LinearDivision(blockchain, distance, stake) { + const linearDivisionParams = await this.blockchainModuleManager.getLinearDivisionParams( + blockchain, + ); + const { distanceScaleFactor, w1, w2 } = linearDivisionParams; + + let dividend = distance; + let divisor = HASH_RING_SIZE.div(2); + if (dividend.gt(UINT128_MAX_BN) || divisor.gt(UINT128_MAX_BN)) { + dividend = dividend.div(distanceScaleFactor); + divisor = divisor.div(distanceScaleFactor); + } + + const divResult = dividend.mul(distanceScaleFactor).div(divisor); + const mappedDistance = + parseFloat(divResult.toString()) / parseFloat(distanceScaleFactor.toString()); + + const maxStake = await this.blockchainModuleManager.getMaximumStake(blockchain); + const mappedStake = + Math.log(stake + 1) / + Math.log(1 + Number(await this.blockchainModuleManager.convertFromWei(maxStake))); + + const proximityScore = w1 * (1 - mappedDistance); + const stakeScore = w2 * mappedStake; + + return { mappedDistance, mappedStake, score: stakeScore / proximityScore }; + } + + async LinearEMANormalization(blockchain, distance, stake, nodeDistanceEMA) { + // const linearEMANormalizationParams = await this.blockchainModuleManager.getLinearEMANormalizationParams(blockchain); + // const { w1, w2 } = linearEMANormalizationParams; + + const w1 = 2; + const w2 = 1; + + const distanceScaleFactor = '1000000000000000000'; + + let dividend = distance; + let divisor = nodeDistanceEMA; + if (dividend.gt(UINT128_MAX_BN) || divisor.gt(UINT128_MAX_BN)) { + dividend = dividend.div(distanceScaleFactor); + divisor = divisor.div(distanceScaleFactor); + } + + const divResult = dividend.mul(distanceScaleFactor).div(divisor); + const mappedDistance = + parseFloat(divResult.toString()) / parseFloat(distanceScaleFactor.toString()); + + const maxStake = await this.blockchainModuleManager.getMaximumStake(blockchain); + const mappedStake = + stake / Number(await this.blockchainModuleManager.convertFromWei(maxStake)); + + const proximityScore = w1 * (1 - mappedDistance); + const stakeScore = w2 * mappedStake; + + return { mappedDistance, mappedStake, score: proximityScore + stakeScore }; } } diff --git a/tools/knowledge-assets-distribution-simulation/mocks/blockchain-module-manager-mock.js b/tools/knowledge-assets-distribution-simulation/mocks/blockchain-module-manager-mock.js index a4488d398b..3a285927d7 100644 --- a/tools/knowledge-assets-distribution-simulation/mocks/blockchain-module-manager-mock.js +++ b/tools/knowledge-assets-distribution-simulation/mocks/blockchain-module-manager-mock.js @@ -9,6 +9,10 @@ class BlockchainModuleManagerMock { return ethers.utils.hexlify(uint8Array); } + convertFromWei(value, toUnit = 'ether') { + return ethers.utils.formatUnits(value, toUnit); + } + convertToWei(blockchain, value, fromUnit = 'ether') { return ethers.utils.parseUnits(value.toString(), fromUnit).toString(); } @@ -17,6 +21,10 @@ class BlockchainModuleManagerMock { return ethers.BigNumber.from(value); } + getMaximumStake(blockchain) { + return '1000000000000000000000000'; + } + getLog2PLDSFParams(blockchain) { return { distanceMappingCoefficient: @@ -43,6 +51,14 @@ class BlockchainModuleManagerMock { w2: 1, }; } + + getLinearSumParams(blockchain) { + return { distanceScaleFactor: '1000000000000000000', w1: 1, w2: 0.25 }; + } + + getLinearDivisionParams(blockchain) { + return { distanceScaleFactor: '1000000000000000000', w1: 1, w2: 0.1 }; + } } export default BlockchainModuleManagerMock; diff --git a/tools/knowledge-assets-distribution-simulation/simulation.js b/tools/knowledge-assets-distribution-simulation/simulation.js index ab98319de7..bb14a79855 100644 --- a/tools/knowledge-assets-distribution-simulation/simulation.js +++ b/tools/knowledge-assets-distribution-simulation/simulation.js @@ -286,6 +286,119 @@ function generateScatterPlot(data, metric, outputImageName) { convertSvgToJpg(d3n.svgString(), outputImageName); } +function generateBoxPlot(data, metric, outputImageName) { + const d3n = new D3Node(); + const margin = { top: 60, right: 30, bottom: 60, left: 60 }; + const width = 2000 - margin.left - margin.right; + const height = 1000 - margin.top - margin.bottom; + + const svg = d3n + .createSVG(width + margin.left + margin.right, height + margin.top + margin.bottom) + .append('g') + .attr('transform', `translate(${margin.left},${margin.top})`); + + svg.append('text') + .attr('x', width / 2) + .attr('y', 0 - margin.top / 2) + .attr('text-anchor', 'middle') + .style('font-size', '20px') + .style('text-decoration', 'underline') + .text(`${metric[0].toUpperCase() + metric.slice(1)} Distribution Plot`); + + const groupedData = d3.group(data, (d) => d.nodeId); + const valuesPerNode = Array.from(groupedData.values(), (d) => d.map((item) => item[metric])); + + const x = d3 + .scaleBand() + .domain(valuesPerNode.map((_, i) => i)) + .range([0, width]); + + svg.append('text') + .attr('transform', `translate(${width / 2}, ${height + margin.top - 20})`) + .style('text-anchor', 'middle') + .text('Node Index'); + + const y = d3 + .scaleLinear() + .domain([0, d3.max(data, (d) => d[metric])]) + .nice() + .range([height, 0]); + + svg.append('text') + .attr('transform', 'rotate(-90)') + .attr('y', 0 - margin.left) + .attr('x', 0 - height / 2) + .attr('dy', '1em') + .style('text-anchor', 'middle') + .text(metric[0].toUpperCase() + metric.slice(1)); + + svg.append('g') + .attr('transform', `translate(0, ${height})`) + .call(d3.axisBottom(x)) + .selectAll('text') + .style('text-anchor', 'end') + .attr('dx', '-.8em') + .attr('dy', '.15em') + .attr('transform', 'rotate(-65)'); + + svg.append('g').call(d3.axisLeft(y).ticks(30)); + + const boxWidth = x.bandwidth() - 3; + + valuesPerNode.forEach((d, i) => { + const box = svg + .append('g') + .attr('transform', `translate(${x(i) + (x.bandwidth() - boxWidth) / 2},0)`); + + const q1 = d3.quantile(d, 0.25); + const median = d3.quantile(d, 0.5); + const q3 = d3.quantile(d, 0.75); + const iqr = q3 - q1; + const lowerLimit = q1 - 1.5 * iqr; + const upperLimit = q3 + 1.5 * iqr; + + const lowerWhisker = d3.max([d3.min(data, (n) => n[metric]), lowerLimit]); + const upperWhisker = d3.min([d3.max(data, (n) => n[metric]), upperLimit]); + + box.append('rect') + .attr('y', y(q3)) + .attr('height', y(q1) - y(q3)) + .attr('width', boxWidth) + .style('fill', '#69b3a2'); + + box.append('line') + .attr('y1', y(median)) + .attr('y2', y(median)) + .attr('x1', 0) + .attr('x2', boxWidth) + .style('stroke', 'black') + .style('width', 80); + + box.append('line') + .attr('y1', y(lowerWhisker)) + .attr('y2', y(upperWhisker)) + .attr('x1', boxWidth / 2) + .attr('x2', boxWidth / 2) + .style('stroke', 'black'); + + box.append('line') + .attr('y1', y(lowerWhisker)) + .attr('y2', y(lowerWhisker)) + .attr('x1', boxWidth * 0.25) + .attr('x2', boxWidth * 0.75) + .style('stroke', 'black'); + + box.append('line') + .attr('y1', y(upperWhisker)) + .attr('y2', y(upperWhisker)) + .attr('x1', boxWidth * 0.25) + .attr('x2', boxWidth * 0.75) + .style('stroke', 'black'); + }); + + convertSvgToJpg(d3n.svgString(), outputImageName); +} + async function runSimulation( mode, filePath, @@ -316,9 +429,12 @@ async function runSimulation( ); const knowledgeAssets = await generateRandomHashes(numberOfKAs); - const distances = []; + const metrics = []; const replicas = {}; + const N = 100; + const EMAs = {}; + for (const node of nodes) { replicas[node.nodeId] = { stake: Number(node.stake), @@ -327,32 +443,48 @@ async function runSimulation( }; } + const positions = await Promise.all( + nodes.map(async (node) => blockchainModuleManagerMock.toBigNumber(blockchain, node.sha256)), + ); + const sortedPositions = positions.sort((a, b) => a.sub(b)); + for (const key of knowledgeAssets) { + const closestNode = sortedPositions.reduce((prev, curr) => { + const diffCurr = curr.sub(key).abs(); + const diffPrev = prev.sub(key).abs(); + + return diffCurr.lt(diffPrev) ? curr : prev; + }); + + const closestNodeIndex = sortedPositions.findIndex((pos) => pos.eq(closestNode)); + const nodesWithDistances = await Promise.all( nodes.map(async (node) => { - const distance = await proximityScoringService.callProximityFunction( - blockchain, - proximityScoreFunctionsPairId, - node.sha256, - key, - ); - - distances.push({ nodeId: node.nodeId, assertionId: key, distance }); + const peerIndex = sortedPositions.findIndex((pos) => pos.eq(node.sha256)); + + let distance; + if (proximityScoreFunctionsPairId === 6) { + const directDistance = Math.abs(closestNodeIndex - peerIndex); + const wraparoundDistance = positions.length - directDistance; + distance = await blockchainModuleManagerMock.toBigNumber( + blockchain, + Math.min(directDistance, wraparoundDistance), + ); + } else { + distance = await proximityScoringService.callProximityFunction( + blockchain, + proximityScoreFunctionsPairId, + node.sha256, + key, + ); + } return { ...node, distance }; }), ); const nodesSortedByDistance = nodesWithDistances - .sort((a, b) => { - if (a.distance.lt(b.distance)) { - return -1; - } - if (a.distance.gt(b.distance)) { - return 1; - } - return 0; - }) + .sort((a, b) => a.distance.sub(b.distance)) .slice(0, r2); const maxDistance = nodesSortedByDistance[nodesSortedByDistance.length - 1].distance; @@ -363,13 +495,26 @@ async function runSimulation( const nodesWithScores = await Promise.all( nodesSortedByDistance.map(async (node) => { - const score = await proximityScoringService.callScoreFunction( - blockchain, - proximityScoreFunctionsPairId, - node.distance, - node.stake, - maxDistance, - ); + const { mappedDistance, mappedStake, score } = + await proximityScoringService.callScoreFunction( + blockchain, + proximityScoreFunctionsPairId, + node.distance, + node.stake, + maxDistance, + EMAs[node.nodeId], + ); + + metrics.push({ nodeId: node.nodeId, mappedDistance, mappedStake, score }); + + if (!(node.nodeId in EMAs)) { + EMAs[node.nodeId] = node.distance; + } else { + EMAs[node.nodeId] = node.distance + .mul(2) + .div(N + 1) + .add(EMAs[node.nodeId].mul(N - 1).div(N + 1)); + } return { ...node, score }; }), @@ -396,6 +541,17 @@ async function runSimulation( 'won', `${mode}-${nodes.length}-${numberOfKAs}-${proximityScoreFunctionsPairId}-stake-wins-relation`, ); + + generateBoxPlot( + metrics, + 'mappedDistance', + `${mode}-${nodes.length}-${numberOfKAs}-${proximityScoreFunctionsPairId}-mapped-distances-distribution`, + ); + generateBoxPlot( + metrics, + 'score', + `${mode}-${nodes.length}-${numberOfKAs}-${proximityScoreFunctionsPairId}-scores-distribution`, + ); } const args = process.argv.slice(2);