From 06f9ba1c9fadd048f6d626dd871464cfab7d3c86 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Tue, 24 Mar 2026 01:59:54 +0100 Subject: [PATCH] Expose merge-mining block classifications --- lib/api.js | 85 ++++++++++++++++++++++++++++++++++++++++++++ lib/blockUnlocker.js | 15 ++++++-- lib/pool.js | 21 +++++++++-- 3 files changed, 116 insertions(+), 5 deletions(-) diff --git a/lib/api.js b/lib/api.js index 1cd0a18..87367e6 100644 --- a/lib/api.js +++ b/lib/api.js @@ -79,6 +79,9 @@ function handleServerRequest (request, response) { case '/get_blocks': handleGetBlocks(urlParts, response); break; + case '/get_mm_blocks': + handleGetMergeMiningBlocks(urlParts, response); + break; // Get market prices case '/get_market': @@ -1034,6 +1037,88 @@ function handleGetBlocks (urlParts, response) { }); } +function parsePoolBlockEntry (serialized, score, source) { + let block = serialized.split(':'); + let rewardType = block[0]; + let base = { + source: source, + height: parseInt(score), + rewardType: rewardType, + miner: rewardType === 'solo' || rewardType === 'prop' + ? `${block[1].substring(0, 7)}...${block[1].substring(block[1].length - 7)}` + : block[1], + hash: block[2], + timestamp: parseInt(block[3]), + difficulty: parseInt(block[4]), + shares: parseInt(block[5]), + orphaned: false, + reward: null, + mmClassification: 'plain', + auxAccepted: false, + parentAccepted: false + }; + + if (source === 'candidate') { + base.score = block[6] ? parseFloat(block[6]) : parseInt(block[5]); + base.mmClassification = block[7] || 'plain'; + base.auxAccepted = block[8] === '1'; + base.parentAccepted = block[9] === '1'; + return base; + } + + base.orphaned = block[6] === '1'; + let hasReward = block.length >= 8 && /^\d+$/.test(block[7]); + base.reward = hasReward ? block[7] : null; + let mmIndex = hasReward ? 8 : 7; + base.mmClassification = block[mmIndex] || 'plain'; + base.auxAccepted = block[mmIndex + 1] === '1'; + base.parentAccepted = block[mmIndex + 2] === '1'; + return base; +} + +function handleGetMergeMiningBlocks (urlParts, response) { + let fromHeight = parseInt(urlParts.query.from_height || 0); + let limit = parseInt(urlParts.query.limit || config.api.blocks || 100); + if (isNaN(fromHeight) || fromHeight < 0) fromHeight = 0; + if (isNaN(limit) || limit <= 0) limit = config.api.blocks || 100; + + let redisCommands = [ + ['zrevrangebyscore', `${config.coin}:blocks:candidates`, '+inf', fromHeight, 'WITHSCORES', 'LIMIT', 0, limit], + ['zrevrangebyscore', `${config.coin}:blocks:matured`, '+inf', fromHeight, 'WITHSCORES', 'LIMIT', 0, limit] + ]; + + redisClient.multi(redisCommands).exec(function (err, replies) { + let data; + if (err) { + data = { error: 'Query failed' }; + } else { + let items = []; + let candidates = replies[0] || []; + let matured = replies[1] || []; + + for (let i = 0; i < candidates.length; i += 2) { + items.push(parsePoolBlockEntry(candidates[i], candidates[i + 1], 'candidate')); + } + for (let i = 0; i < matured.length; i += 2) { + items.push(parsePoolBlockEntry(matured[i], matured[i + 1], 'matured')); + } + + data = { + items: items + }; + } + + let reply = JSON.stringify(data); + response.writeHead("200", { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-cache', + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(reply, 'utf8') + }); + response.end(reply); + }); +} + /** * Get market exchange prices **/ diff --git a/lib/blockUnlocker.js b/lib/blockUnlocker.js index 0d537af..5d1e9ac 100644 --- a/lib/blockUnlocker.js +++ b/lib/blockUnlocker.js @@ -53,7 +53,10 @@ function runInterval () { time: parts[3], difficulty: parts[4], shares: parts[5], - score: parts.length >= 7 ? parts[6] : parts[5] + score: parts.length >= 7 ? parts[6] : parts[5], + mmClassification: parts.length >= 8 ? parts[7] : 'plain', + auxAccepted: parts.length >= 9 ? parts[8] : 0, + parentAccepted: parts.length >= 10 ? parts[9] : 0 }); } callback(null, blocks); @@ -159,7 +162,10 @@ function runInterval () { block.time, block.difficulty, block.shares, - block.orphaned + block.orphaned, + block.mmClassification || 'plain', + block.auxAccepted || 0, + block.parentAccepted || 0 ].join(':')]); if (block.workerScores && !slushMiningEnabled) { @@ -217,7 +223,10 @@ function runInterval () { block.difficulty, block.shares, block.orphaned, - block.reward + block.reward, + block.mmClassification || 'plain', + block.auxAccepted || 0, + block.parentAccepted || 0 ].join(':')]); let feePercent = (config.blockUnlocker.poolFee > 0 ? config.blockUnlocker.poolFee : 0) / 100; diff --git a/lib/pool.js b/lib/pool.js index 5b23ddd..fc57ad2 100644 --- a/lib/pool.js +++ b/lib/pool.js @@ -881,6 +881,20 @@ function recordShareData (miner, job, shareDiff, blockCandidate, hashHex, shareT let candidateHash = blockMeta && blockMeta.hash ? blockMeta.hash : hashHex; let workerName = miner.workerName; let rewardType = miner.rewardType; + let mmClassification = 'plain'; + let auxAccepted = 0; + let parentAccepted = 0; + + if (arguments.length >= 9 && arguments[8]) { + let submitResult = arguments[8]; + auxAccepted = submitResult.aux_accepted ? 1 : 0; + parentAccepted = submitResult.parent_accepted ? 1 : 0; + if (auxAccepted && parentAccepted) { + mmClassification = 'dual-mm'; + } else if (auxAccepted) { + mmClassification = 'aux-mm'; + } + } let updateScore; // Weighting older shares lower than newer ones to prevent pool hopping if (slushMiningEnabled) { @@ -991,7 +1005,10 @@ function recordShareData (miner, job, shareDiff, blockCandidate, hashHex, shareT Date.now() / 1000 | 0, blockTemplate.difficulty, totalShares, - totalScore + totalScore, + mmClassification, + auxAccepted, + parentAccepted ].join(':'), function (err, result) { if (err) { log('error', logSystem, 'Failed inserting block candidate %s \n %j', [candidateHash, err]); @@ -1103,7 +1120,7 @@ function processShare (miner, job, blockTemplate, params) { 'Block %s found at height %d by miner %s@%s - submit result: %j', [blockFastHash.substr(0, 6), job.height, miner.login, miner.ip, result] ); - recordShareData(miner, job, hashDiff.toString(), true, blockFastHash, shareType, blockTemplate, acceptedBlockMeta); + recordShareData(miner, job, hashDiff.toString(), true, blockFastHash, shareType, blockTemplate, acceptedBlockMeta, result); } }, miningBackends.getMiningRpcConfig(), submitHeaders); } else if (hashDiff < jobDifficulty) {