Skip to content

Instantly share code, notes, and snippets.

@Timonchegs
Created December 24, 2025 04:08
Show Gist options
  • Select an option

  • Save Timonchegs/1ab8231719adde52a3950a23ee558e02 to your computer and use it in GitHub Desktop.

Select an option

Save Timonchegs/1ab8231719adde52a3950a23ee558e02 to your computer and use it in GitHub Desktop.
gemini
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