-
-
Save Timonchegs/1ab8231719adde52a3950a23ee558e02 to your computer and use it in GitHub Desktop.
gemini
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const net = require('net'); | |
| const jayson = require('jayson/promise'); | |
| // --- КОНФИГУРАЦИЯ (ВАШИ ДАННЫЕ) --- | |
| const PROXY_PORT = 3333; | |
| // Подключаемся локально, так как скрипт и нода на одной машине (192.168.2.180) | |
| // Используем порт 8668, так как он указан для rpclistengetwork | |
| const NODE_RPC_URL = 'http://127.0.0.1:8668'; | |
| const NODE_RPC_USER = 'tteFTlJ7YOfGDA2KBMHKqnDnXeE='; | |
| const NODE_RPC_PASS = 'SOkvF8sxay8ViOxpgbraHmqJmSU='; | |
| const POLL_INTERVAL = 500; // Интервал опроса ноды (мс) | |
| // Клиент для общения с нодой Abelian | |
| const client = jayson.client.http({ | |
| url: NODE_RPC_URL, | |
| auth: { | |
| username: NODE_RPC_USER, | |
| password: NODE_RPC_PASS | |
| } | |
| }); | |
| // Глобальное состояние | |
| let currentJob = null; | |
| let clients = []; | |
| let jobIdCounter = 1000; | |
| // --- ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ --- | |
| // Функция для переворота байт (Little Endian <-> Big Endian) | |
| // f0000000018f2f21 -> 212f8f01000000f0 | |
| function reverseHex(hex) { | |
| if (!hex) return ""; | |
| // Убираем префикс 0x если есть | |
| hex = hex.replace(/^0x/, ''); | |
| if (hex.length % 2 !== 0) hex = '0' + hex; | |
| let reversed = ''; | |
| for (let i = hex.length - 2; i >= 0; i -= 2) { | |
| reversed += hex.substr(i, 2); | |
| } | |
| return reversed; | |
| } | |
| // Формирование сложности (Target) для Stratum | |
| // Стандартный высокий target для майнера (diff 1) | |
| const DEFAULT_TARGET = "00000000ffff0000000000000000000000000000000000000000000000000000"; | |
| // --- ЛОГИКА РАБОТЫ С НОДОЙ (GETWORK) --- | |
| async function fetchWork() { | |
| try { | |
| const response = await client.request('getwork', []); | |
| if (response.result) { | |
| // Структура ответа getwork Abelian: | |
| // [0] - Header Hash (используется как work package) | |
| // [1] - Seed Hash (Epoch) | |
| // [2] - Target boundary | |
| // [3] - Job ID (от ноды) | |
| const r = response.result; | |
| // Проверка на корректность ответа | |
| if (!r || r.length < 4) return; | |
| const newNodeJobId = r[3]; | |
| // Если работа изменилась или это первый запуск | |
| if (!currentJob || currentJob.nodeJobId !== newNodeJobId) { | |
| console.log(`[NODE] New Job received: ID=${newNodeJobId}`); | |
| // Генерируем короткий ID для стратума | |
| const stratumJobId = (jobIdCounter++).toString(16); | |
| currentJob = { | |
| id: stratumJobId, | |
| nodeJobId: newNodeJobId, | |
| contentHash: r[0], // Хеш заголовка для майнинга | |
| seedHash: r[1], // Сид эпохи | |
| target: r[2], // Таргет от ноды | |
| clean: true | |
| }; | |
| broadcastJob(currentJob); | |
| } | |
| } | |
| } catch (err) { | |
| console.error('[NODE] Getwork Error (Check RPC connection):', err.message); | |
| } | |
| } | |
| // Рассылка работы всем подключенным майнерам | |
| function broadcastJob(job) { | |
| const notifyParams = [ | |
| job.id, // Job ID | |
| job.contentHash, // Header Hash (32 bytes hex) | |
| job.seedHash, // Seed Hash (для DAG) | |
| DEFAULT_TARGET, // Target (упрощенный для шары) | |
| true, // Clean Jobs | |
| "0800", // Block height/Version (dummy) | |
| "00" // Extra (dummy) | |
| ]; | |
| const notifyReq = JSON.stringify({ | |
| id: null, | |
| method: "mining.notify", | |
| params: notifyParams | |
| }) + '\n'; | |
| clients.forEach(socket => { | |
| if (!socket.destroyed) { | |
| socket.write(notifyReq); | |
| } | |
| }); | |
| } | |
| // Запускаем опрос ноды | |
| setInterval(fetchWork, POLL_INTERVAL); | |
| // --- STRATUM СЕРВЕР --- | |
| const server = net.createServer((socket) => { | |
| // Получаем IP майнера для логов | |
| const remoteAddr = `${socket.remoteAddress}:${socket.remotePort}`; | |
| console.log(`[STRATUM] Miner connected: ${remoteAddr}`); | |
| clients.push(socket); | |
| let buffer = ''; | |
| socket.on('data', async (chunk) => { | |
| buffer += chunk.toString(); | |
| // Обработка склеенных пакетов | |
| while (buffer.includes('\n')) { | |
| const newlineIndex = buffer.indexOf('\n'); | |
| const line = buffer.slice(0, newlineIndex).trim(); | |
| buffer = buffer.slice(newlineIndex + 1); | |
| if (!line) continue; | |
| try { | |
| const req = JSON.parse(line); | |
| await handleStratumRequest(socket, req); | |
| } catch (e) { | |
| console.error(`[STRATUM] JSON Parse error from ${remoteAddr}:`, e.message); | |
| } | |
| } | |
| }); | |
| socket.on('close', () => { | |
| console.log(`[STRATUM] Miner disconnected: ${remoteAddr}`); | |
| clients = clients.filter(c => c !== socket); | |
| }); | |
| socket.on('error', (err) => { | |
| if (err.code !== 'ECONNRESET') { | |
| console.error(`[STRATUM] Socket error: ${err.message}`); | |
| } | |
| }); | |
| }); | |
| async function handleStratumRequest(socket, req) { | |
| const method = req.method; | |
| const params = req.params; | |
| const id = req.id; | |
| // 1. mining.subscribe | |
| if (method === 'mining.subscribe') { | |
| console.log(`[MINER] Subscribe received`); | |
| const response = { | |
| id: id, | |
| result: [ | |
| [["mining.notify", "ae6812eb4cd7735a302a8a9dd95cf71f"], ["mining.set_difficulty", "ae6812eb4cd7735a302a8a9dd95cf71f"]], | |
| "0800", // ExtraNonce1 (HEX) | |
| 4 // ExtraNonce2 Size | |
| ], | |
| error: null | |
| }; | |
| socket.write(JSON.stringify(response) + '\n'); | |
| // Сразу отправляем сложность и работу | |
| socket.write(JSON.stringify({ method: "mining.set_difficulty", params: [0.1] }) + '\n'); | |
| if (currentJob) { | |
| broadcastJob(currentJob); | |
| } | |
| } | |
| // 2. mining.authorize | |
| else if (method === 'mining.authorize') { | |
| const worker = params[0]; | |
| console.log(`[MINER] Authorize: ${worker}`); | |
| socket.write(JSON.stringify({ id: id, result: true, error: null }) + '\n'); | |
| } | |
| // 3. mining.submit | |
| else if (method === 'mining.submit') { | |
| // TRM Format: [WorkerName, JobID, Nonce] или [JobID, Nonce, WorkerName] | |
| // Из логов: ["2e05", "f0000000018f2f21", "trmtest"] -> JobID, Nonce, Worker | |
| let jobIdStr = params[0]; | |
| let minerNonce = params[1]; | |
| let workerName = params[2]; | |
| // Защита от перепутанных параметров | |
| if (minerNonce.length < 8 && workerName.length > 8) { | |
| let temp = minerNonce; minerNonce = workerName; workerName = temp; | |
| } | |
| console.log(`[SUBMIT] Worker: ${workerName}, Job: ${jobIdStr}, Nonce: ${minerNonce}`); | |
| if (!currentJob || currentJob.id !== jobIdStr) { | |
| console.warn(`[SUBMIT] Stale share (Old Job). Current: ${currentJob ? currentJob.id : 'None'}, Got: ${jobIdStr}`); | |
| socket.write(JSON.stringify({ id: id, result: false, error: [21, "Job not found", null] }) + '\n'); | |
| return; | |
| } | |
| // --- ВАЖНО: ПЕРЕВОРАЧИВАЕМ NONCE --- | |
| const reversedNonce = reverseHex(minerNonce); | |
| // --- ОТПРАВКА В НОДУ --- | |
| // Попытка 1: Отправляем без mixDigest (надеемся, что нода посчитает сама) | |
| let rpcParams = [currentJob.nodeJobId, reversedNonce]; | |
| try { | |
| // console.log(`[RPC] Sending submitwork to port 8668...`, rpcParams); | |
| const result = await client.request('submitwork', rpcParams); | |
| if (result.result === true) { | |
| console.log(`[SUCCESS] Share ACCEPTED! Nonce: ${minerNonce}`); | |
| socket.write(JSON.stringify({ id: id, result: true, error: null }) + '\n'); | |
| } else { | |
| console.warn(`[REJECT] Node rejected. Result:`, result); | |
| socket.write(JSON.stringify({ id: id, result: false, error: [20, "Node rejected share", null] }) + '\n'); | |
| } | |
| } catch (rpcErr) { | |
| // Если ошибка "params length" или "invalid params", пробуем добавить пустой mixDigest | |
| // Это запасной план | |
| if (rpcErr.message && (rpcErr.message.includes("length") || rpcErr.message.includes("params") || rpcErr.message.includes("Internal"))) { | |
| console.log(`[RPC] Retrying with empty MixDigest...`); | |
| try { | |
| // 32 байта нулей | |
| const emptyMixDigest = "0000000000000000000000000000000000000000000000000000000000000000"; | |
| const retryResult = await client.request('submitwork', [currentJob.nodeJobId, reversedNonce, emptyMixDigest]); | |
| if (retryResult.result === true) { | |
| console.log(`[SUCCESS] Retry ACCEPTED!`); | |
| socket.write(JSON.stringify({ id: id, result: true, error: null }) + '\n'); | |
| return; | |
| } | |
| } catch (e) { | |
| console.error(`[RPC RETRY FAILED]`, e.message); | |
| } | |
| } else { | |
| console.error(`[RPC ERROR]`, rpcErr.message); | |
| } | |
| socket.write(JSON.stringify({ id: id, result: null, error: [-1, "Node RPC Error", null] }) + '\n'); | |
| } | |
| } | |
| // 4. extranonce.subscribe | |
| else if (method === 'mining.extranonce.subscribe') { | |
| socket.write(JSON.stringify({ id: id, result: true, error: null }) + '\n'); | |
| } | |
| else { | |
| // Игнорируем неизвестные методы | |
| } | |
| } | |
| // Запуск сервера | |
| server.listen(PROXY_PORT, '0.0.0.0', () => { | |
| console.log(`Stratum Proxy running on port ${PROXY_PORT}`); | |
| console.log(`Listening for external connections on 0.0.0.0:${PROXY_PORT}`); | |
| console.log(`Target Node RPC: ${NODE_RPC_URL}`); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment