#!/usr/bin/env node /** * RIN Coin Stratum Proxy Server * Custom implementation without external stratum library * * This replaces the lost custom proxy implementation */ const net = require('net'); const axios = require('axios'); const crypto = require('crypto'); // Configuration const CONFIG = { // Stratum server settings stratum: { host: '0.0.0.0', port: 3333, difficulty: 1.0 // Production difficulty (auto-adjusts to network) }, // RIN RPC settings rpc: { host: '127.0.0.1', port: 9556, user: 'rinrpc', password: '745ce784d5d537fc06105a1b935b7657903cfc71a5fb3b90' }, // Mining settings mining: { targetAddress: 'rin1qahvvv9d5f3443wtckeqavwp9950wacxfmwv20q', extranonceSize: 4, jobUpdateInterval: 30000 // 30 seconds } }; class RinStratumProxy { constructor() { this.server = null; this.currentJob = null; this.jobCounter = 0; this.clients = new Map(); this.running = false; this.extranonceCounter = 0; console.log('šŸš€ RIN Stratum Proxy Server (Custom Implementation)'); console.log(`šŸ“” Stratum: ${CONFIG.stratum.host}:${CONFIG.stratum.port}`); console.log(`šŸ”— RPC: ${CONFIG.rpc.host}:${CONFIG.rpc.port}`); console.log(`šŸ’° Target: ${CONFIG.mining.targetAddress}`); } /** * Make RPC call to RIN node */ async rpcCall(method, params = []) { try { const url = `http://${CONFIG.rpc.host}:${CONFIG.rpc.port}/`; const auth = Buffer.from(`${CONFIG.rpc.user}:${CONFIG.rpc.password}`).toString('base64'); const response = await axios.post(url, { jsonrpc: '1.0', id: 'stratum_proxy', method: method, params: params }, { headers: { 'Content-Type': 'text/plain', 'Authorization': `Basic ${auth}` }, timeout: 30000 }); if (response.data.error) { console.error(`RPC Error: ${response.data.error}`); return null; } return response.data.result; } catch (error) { console.error(`RPC Call Error: ${error.message}`); return null; } } /** * Convert bits to target (Bitcoin-style) - FIXED VERSION */ bitsToTarget(bitsHex) { try { const bits = parseInt(bitsHex, 16); const exponent = bits >> 24; const mantissa = bits & 0xffffff; // Bitcoin target calculation using BigInt for proper handling let target; if (exponent <= 3) { target = BigInt(mantissa) >> BigInt(8 * (3 - exponent)); } else { target = BigInt(mantissa) << BigInt(8 * (exponent - 3)); } // Ensure we don't exceed 256 bits if (target > (1n << 256n) - 1n) { target = (1n << 256n) - 1n; } return target.toString(16).padStart(64, '0'); } catch (error) { console.error(`Bits to target error: ${error.message}`); return '0000ffff00000000000000000000000000000000000000000000000000000000'; } } /** * Calculate network difficulty from target - FIXED VERSION */ calculateNetworkDifficulty(targetHex) { try { const targetInt = BigInt('0x' + targetHex); // Bitcoin difficulty 1.0 target const diff1Target = 0x00000000FFFF0000000000000000000000000000000000000000000000000000n; // Network difficulty = how much harder than difficulty 1.0 const networkDifficulty = Number(diff1Target / targetInt); return networkDifficulty; } catch (error) { console.error(`Network difficulty calculation error: ${error.message}`); return 1.0; } } /** * Get new block template from RIN node */ async getBlockTemplate() { try { const template = await this.rpcCall('getblocktemplate', [{ rules: ['mweb', 'segwit'] }]); if (!template) { return null; } this.jobCounter++; const job = { jobId: `job_${this.jobCounter.toString(16).padStart(8, '0')}`, template: template, prevhash: template.previousblockhash || '0'.repeat(64), version: template.version || 1, bits: template.bits || '1d00ffff', ntime: Math.floor(Date.now() / 1000).toString(16).padStart(8, '0'), target: this.bitsToTarget(template.bits || '1d00ffff'), height: template.height || 0, coinbasevalue: template.coinbasevalue || 0, transactions: template.transactions || [] }; this.currentJob = job; const timestamp = new Date().toISOString(); const networkDifficulty = this.calculateNetworkDifficulty(job.target); console.log(`[${timestamp}] šŸ†• NEW JOB: ${job.jobId} | Height: ${job.height} | Reward: ${(job.coinbasevalue / 100000000).toFixed(2)} RIN`); console.log(` šŸŽÆ Network Difficulty: ${networkDifficulty.toFixed(6)} | Bits: ${job.bits}`); console.log(` šŸ“ Target: ${job.target.substring(0, 16)}... | Transactions: ${job.transactions.length}`); return job; } catch (error) { console.error(`Get block template error: ${error.message}`); return null; } } /** * Encode integer as Bitcoin-style varint */ encodeVarint(n) { if (n < 0xfd) { return Buffer.from([n]); } else if (n <= 0xffff) { const buf = Buffer.allocUnsafe(3); buf.writeUInt8(0xfd, 0); buf.writeUInt16LE(n, 1); return buf; } else if (n <= 0xffffffff) { const buf = Buffer.allocUnsafe(5); buf.writeUInt8(0xfe, 0); buf.writeUInt32LE(n, 1); return buf; } else { const buf = Buffer.allocUnsafe(9); buf.writeUInt8(0xff, 0); buf.writeBigUInt64LE(BigInt(n), 1); return buf; } } /** * Decode RinCoin bech32 address to script */ async decodeBech32Address(address) { try { if (!address || !address.startsWith('rin1')) { throw new Error('Not a RinCoin bech32 address'); } const result = await this.rpcCall('validateaddress', [address]); if (!result || !result.isvalid) { throw new Error('Address not valid per node'); } const scriptHex = result.scriptPubKey; if (!scriptHex) { throw new Error('Node did not return scriptPubKey'); } return Buffer.from(scriptHex, 'hex'); } catch (error) { console.error(`Address decode error: ${error.message}`); return null; } } /** * Build coinbase transaction (with and without witness) */ async buildCoinbaseTransaction(template, extranonce1, extranonce2, targetAddress) { try { const hasWitnessCommitment = template.default_witness_commitment !== undefined; // Common parts const value = template.coinbasevalue || 0; const scriptPubkey = await this.decodeBech32Address(targetAddress); if (!scriptPubkey) { return { wit: null, nowit: null }; } const witnessCommitment = template.default_witness_commitment; // ScriptSig (block height minimal push + tag + extranonces) const height = template.height || 0; const heightBytes = Buffer.allocUnsafe(4); heightBytes.writeUInt32LE(height, 0); const heightCompact = Buffer.concat([ Buffer.from([heightBytes.length]), heightBytes ]); const scriptsig = Buffer.concat([ heightCompact, Buffer.from('/RinCoin/'), Buffer.from(extranonce1), Buffer.from(extranonce2) ]); // Helper to build outputs blob const buildOutputsBlob = () => { const outputsList = []; // Main output const valueBuffer = Buffer.allocUnsafe(8); valueBuffer.writeBigUInt64LE(BigInt(value), 0); outputsList.push(Buffer.concat([ valueBuffer, this.encodeVarint(scriptPubkey.length), scriptPubkey ])); // Witness commitment OP_RETURN output if present if (witnessCommitment) { const commitScript = Buffer.from(witnessCommitment, 'hex'); const zeroValue = Buffer.allocUnsafe(8); zeroValue.writeBigUInt64LE(0n, 0); outputsList.push(Buffer.concat([ zeroValue, this.encodeVarint(commitScript.length), commitScript ])); } const outputsBlob = Buffer.concat([ this.encodeVarint(outputsList.length), ...outputsList ]); return outputsBlob; }; // Build non-witness serialization (txid serialization) const versionBuffer = Buffer.allocUnsafe(4); versionBuffer.writeUInt32LE(1, 0); const prevoutHash = Buffer.alloc(32); const prevoutIndex = Buffer.from([0xff, 0xff, 0xff, 0xff]); const sequence = Buffer.from([0xff, 0xff, 0xff, 0xff]); const locktime = Buffer.allocUnsafe(4); locktime.writeUInt32LE(0, 0); const cbNowit = Buffer.concat([ versionBuffer, // version Buffer.from([0x01]), // input count prevoutHash, // prevout hash prevoutIndex, // prevout index this.encodeVarint(scriptsig.length), // scriptsig length scriptsig, // scriptsig sequence, // sequence buildOutputsBlob(), // outputs locktime // locktime ]); // Build with-witness serialization (block serialization) let cbWit; if (hasWitnessCommitment) { const witnessStack = Buffer.concat([ Buffer.from([0x01]), // witness stack count Buffer.from([0x20]), // item length Buffer.alloc(32) // reserved value ]); cbWit = Buffer.concat([ versionBuffer, // version Buffer.from([0x00, 0x01]), // segwit marker+flag Buffer.from([0x01]), // input count prevoutHash, // prevout hash prevoutIndex, // prevout index this.encodeVarint(scriptsig.length), // scriptsig length scriptsig, // scriptsig sequence, // sequence buildOutputsBlob(), // outputs witnessStack, // witness locktime // locktime ]); } else { cbWit = cbNowit; } return { wit: cbWit, nowit: cbNowit }; } catch (error) { console.error(`Coinbase construction error: ${error.message}`); return { wit: null, nowit: null }; } } /** * Calculate merkle root with coinbase at index 0 */ calculateMerkleRoot(coinbaseTxid, transactions) { try { // Start with all transaction hashes (coinbase + others) const hashes = [coinbaseTxid]; for (const tx of transactions) { // Reverse for little-endian const txHash = Buffer.from(tx.hash, 'hex').reverse(); hashes.push(txHash); } // Build merkle tree while (hashes.length > 1) { if (hashes.length % 2 === 1) { hashes.push(hashes[hashes.length - 1]); // Duplicate last hash if odd } const nextLevel = []; for (let i = 0; i < hashes.length; i += 2) { const combined = Buffer.concat([hashes[i], hashes[i + 1]]); const hash1 = crypto.createHash('sha256').update(combined).digest(); const hash2 = crypto.createHash('sha256').update(hash1).digest(); nextLevel.push(hash2); } hashes.splice(0, hashes.length, ...nextLevel); } return hashes[0] || Buffer.alloc(32); } catch (error) { console.error(`Merkle root calculation error: ${error.message}`); return Buffer.alloc(32); } } /** * Calculate share difficulty from hash */ calculateShareDifficulty(hashHex) { try { const hashInt = BigInt('0x' + hashHex); if (hashInt === 0n) { return Number.POSITIVE_INFINITY; // Perfect hash } // Bitcoin-style difficulty calculation using difficulty 1 target const diff1Target = 0x00000000FFFF0000000000000000000000000000000000000000000000000000n; // Share difficulty = how much harder this hash was compared to diff 1 const difficulty = Number(diff1Target / hashInt); return difficulty; } catch (error) { console.error(`Difficulty calculation error: ${error.message}`); return 0.0; } } /** * Validate and submit share - FULL PRODUCTION VERSION */ async submitShare(job, extranonce1, extranonce2, ntime, nonce, targetAddress = null) { try { const address = targetAddress || CONFIG.mining.targetAddress; // Build coinbase (with and without witness) const coinbase = await this.buildCoinbaseTransaction( job.template, extranonce1, extranonce2, address); if (!coinbase.wit || !coinbase.nowit) { return { success: false, message: 'Coinbase construction failed' }; } // Calculate coinbase txid (non-witness serialization) const hash1 = crypto.createHash('sha256').update(coinbase.nowit).digest(); const hash2 = crypto.createHash('sha256').update(hash1).digest(); const coinbaseTxid = hash2.reverse(); // Reverse for little-endian // Calculate merkle root const merkleRoot = this.calculateMerkleRoot(coinbaseTxid, job.transactions); // Build block header - FIXED ENDIANNESS const versionBuffer = Buffer.allocUnsafe(4); versionBuffer.writeUInt32LE(job.version, 0); const prevhashBuffer = Buffer.from(job.prevhash, 'hex').reverse(); // big-endian in block const ntimeBuffer = Buffer.allocUnsafe(4); ntimeBuffer.writeUInt32LE(parseInt(ntime, 16), 0); const bitsBuffer = Buffer.from(job.bits, 'hex').reverse(); // big-endian in block const nonceBuffer = Buffer.allocUnsafe(4); nonceBuffer.writeUInt32LE(parseInt(nonce, 16), 0); const header = Buffer.concat([ versionBuffer, // Version (little-endian) prevhashBuffer, // Previous block hash (big-endian in block) merkleRoot, // Merkle root (already in correct endian) ntimeBuffer, // Timestamp (little-endian) bitsBuffer, // Bits (big-endian in block) nonceBuffer // Nonce (little-endian) ]); // Calculate block hash - FIXED DOUBLE SHA256 const blockHash1 = crypto.createHash('sha256').update(header).digest(); const blockHash2 = crypto.createHash('sha256').update(blockHash1).digest(); const blockHashHex = blockHash2.reverse().toString('hex'); // Reverse for display/comparison // Calculate real difficulties const shareDifficulty = this.calculateShareDifficulty(blockHashHex); const networkDifficulty = this.calculateNetworkDifficulty(job.target); // Check if hash meets target - FIXED COMPARISON const hashInt = BigInt('0x' + blockHashHex); const targetInt = BigInt('0x' + job.target); const meetsTarget = hashInt <= targetInt; // FIXED: less than or equal // Enhanced logging const timestamp = new Date().toISOString(); const difficultyPercentage = networkDifficulty > 0 ? (shareDifficulty / networkDifficulty) * 100 : 0; // Progress indicator based on percentage let progressIcon; if (meetsTarget) { progressIcon = 'šŸŽ‰'; // Block found! } else if (difficultyPercentage >= 50) { progressIcon = 'šŸ”„'; // Very close } else if (difficultyPercentage >= 10) { progressIcon = '⚔'; // Getting warm } else if (difficultyPercentage >= 1) { progressIcon = 'šŸ’«'; // Some progress } else { progressIcon = 'šŸ“Š'; // Low progress } console.log(`[${timestamp}] ${progressIcon} SHARE: job=${job.jobId} | nonce=${nonce} | hash=${blockHashHex.substring(0, 16)}...`); console.log(` šŸŽÆ Share Diff: ${shareDifficulty.toExponential(2)} | Network Diff: ${networkDifficulty.toFixed(6)}`); console.log(` šŸ“ˆ Progress: ${difficultyPercentage.toFixed(4)}% of network difficulty`); console.log(` šŸ“ Target: ${job.target.substring(0, 16)}... | Height: ${job.height}`); console.log(` ā° Time: ${ntime} | Extranonce: ${extranonce1}:${extranonce2}`); console.log(` šŸ” Hash vs Target: ${hashInt.toString()} ${meetsTarget ? '<=' : '>'} ${targetInt.toString()}`); if (!meetsTarget) { // Share doesn't meet target - reject but still useful for debugging console.log(` āŒ Share rejected (hash > target)`); return { success: false, message: 'Share too high' }; } // Valid block! Build full block and submit console.log(` šŸŽ‰ BLOCK FOUND! Hash: ${blockHashHex}`); console.log(` šŸ’° Reward: ${(job.coinbasevalue / 100000000).toFixed(2)} RIN -> ${address}`); console.log(` šŸ“Š Block height: ${job.height}`); console.log(` šŸ” Difficulty: ${shareDifficulty.toFixed(6)} (target: ${networkDifficulty.toFixed(6)})`); // Build complete block const txCount = 1 + job.transactions.length; const block = Buffer.concat([ header, this.encodeVarint(txCount), coinbase.wit // Add coinbase transaction (witness variant for block body) ]); // Add other transactions const txBuffers = []; for (const tx of job.transactions) { txBuffers.push(Buffer.from(tx.data, 'hex')); } const fullBlock = Buffer.concat([block, ...txBuffers]); // Submit block const blockHex = fullBlock.toString('hex'); console.log(` šŸ“¦ Submitting block of size ${fullBlock.length} bytes...`); const result = await this.rpcCall('submitblock', [blockHex]); if (result === null) { console.log(` āœ… Block accepted by network!`); return { success: true, message: 'Block found and submitted' }; } else { console.log(` āŒ Block rejected: ${result}`); console.log(` šŸ” Debug: Block size ${fullBlock.length} bytes, ${job.transactions.length} transactions`); return { success: false, message: `Block rejected: ${result}` }; } } catch (error) { console.error(`Share submission error: ${error.message}`); return { success: false, message: `Submission error: ${error.message}` }; } } /** * Send Stratum response to client */ sendResponse(client, id, result, error = null) { try { const response = { id: id, result: result, error: error }; const message = JSON.stringify(response) + '\n'; client.write(message); } catch (error) { console.error(`Send response error: ${error.message}`); } } /** * Send Stratum notification to client */ sendNotification(client, method, params) { try { const notification = { id: null, method: method, params: params }; const message = JSON.stringify(notification) + '\n'; client.write(message); } catch (error) { console.error(`Send notification error: ${error.message}`); } } /** * Handle Stratum message from client */ async handleMessage(client, addr, message) { try { const data = JSON.parse(message.trim()); const method = data.method; const id = data.id; const params = data.params || []; console.log(`šŸ“Ø [${addr}] ${method}: ${JSON.stringify(params)}`); if (method === 'mining.subscribe') { // Generate unique extranonce1 for this connection this.extranonceCounter++; const extranonce1 = this.extranonceCounter.toString(16).padStart(8, '0'); // Store client info this.clients.set(client, { addr: addr, extranonce1: extranonce1, username: null }); // Send subscription response (cpuminer expects specific format) this.sendResponse(client, id, [ [['mining.set_difficulty', 'subscription_id'], ['mining.notify', 'subscription_id']], extranonce1, 4 // extranonce2 size ]); console.log(`šŸ“ [${addr}] Subscription response sent: extranonce1=${extranonce1}`); // Send extranonce1 notification immediately (cpuminer expects this first) this.sendNotification(client, 'mining.set_extranonce', [extranonce1, 4]); // Send difficulty this.sendNotification(client, 'mining.set_difficulty', [CONFIG.stratum.difficulty]); // Send initial job if available if (this.currentJob) { this.sendJobToClient(client); } } else if (method === 'mining.authorize') { const username = params[0] || 'anonymous'; const clientInfo = this.clients.get(client); if (clientInfo) { clientInfo.username = username; } this.sendResponse(client, id, true); console.log(`šŸ” [${addr}] Authorized as ${username}`); // Send current job after authorization if (this.currentJob) { this.sendJobToClient(client); } } else if (method === 'mining.extranonce.subscribe') { // Handle extranonce subscription this.sendResponse(client, id, true); console.log(`šŸ“ [${addr}] Extranonce subscription accepted`); } else if (method === 'mining.submit') { if (params.length >= 5) { const [username, jobId, extranonce2, ntime, nonce] = params; console.log(`šŸ“Š [${addr}] Submit: ${username} | job=${jobId} | nonce=${nonce}`); if (this.currentJob) { const clientInfo = this.clients.get(client); const extranonce1 = clientInfo ? clientInfo.extranonce1 : '00000000'; // Submit share const result = await this.submitShare(this.currentJob, extranonce1, extranonce2, ntime, nonce); // Always accept shares for debugging this.sendResponse(client, id, true); if (result.success && result.message.includes('Block found')) { // Get new job after block found setTimeout(() => this.updateJobAfterBlock(), 2000); } } else { this.sendResponse(client, id, true); } } else { this.sendResponse(client, id, false, 'Invalid parameters'); } } else { console.log(`ā“ [${addr}] Unknown method: ${method}`); this.sendResponse(client, id, null, 'Unknown method'); } } catch (error) { console.error(`[${addr}] Message handling error: ${error.message}`); this.sendResponse(client, null, null, 'Invalid JSON'); } } /** * Send mining job to specific client */ sendJobToClient(client) { if (!this.currentJob) { return; } try { // Send proper stratum mining.notify with all required fields this.sendNotification(client, 'mining.notify', [ this.currentJob.jobId, // job_id this.currentJob.prevhash, // prevhash '', // coinb1 (empty - miner builds coinbase) '', // coinb2 (empty - miner builds coinbase) [], // merkle_branch (empty - we calculate merkle root) this.currentJob.version.toString(16).padStart(8, '0'), // version this.currentJob.bits, // nbits this.currentJob.ntime, // ntime true // clean_jobs ]); // Also send the block height and transaction count as custom notification // This helps miners display correct information this.sendNotification(client, 'mining.set_extranonce', [ this.currentJob.height, this.currentJob.transactions.length ]); } catch (error) { console.error(`Failed to send job: ${error.message}`); } } /** * Update job after block found */ async updateJobAfterBlock() { console.log('šŸ”„ Updating job after block found...'); if (await this.getBlockTemplate()) { this.broadcastNewJob(); } } /** * Broadcast new job to all connected clients */ broadcastNewJob() { if (!this.currentJob) { return; } console.log(`šŸ“¢ Broadcasting new job to ${this.clients.size} clients`); for (const [client, clientInfo] of this.clients) { try { this.sendJobToClient(client); } catch (error) { console.error(`Failed to send job to ${clientInfo.addr}: ${error.message}`); } } } /** * Handle client connection */ handleClient(client, addr) { console.log(`šŸ”Œ [${addr}] Connected`); client.on('data', (data) => { // Handle multiple messages in one packet const messages = data.toString().trim().split('\n'); for (const message of messages) { if (message) { this.handleMessage(client, addr, message); } } }); client.on('close', () => { console.log(`šŸ”Œ [${addr}] Disconnected`); this.clients.delete(client); }); client.on('error', (error) => { console.error(`āŒ [${addr}] Client error: ${error.message}`); this.clients.delete(client); }); } /** * Start job updater */ startJobUpdater() { setInterval(async () => { try { const oldHeight = this.currentJob ? this.currentJob.height : 0; if (await this.getBlockTemplate()) { const newHeight = this.currentJob.height; if (newHeight > oldHeight) { console.log('šŸ”„ New block detected! Broadcasting new job...'); this.broadcastNewJob(); } } } catch (error) { console.error(`Job updater error: ${error.message}`); } }, CONFIG.mining.jobUpdateInterval); } /** * Start the stratum server */ async start() { try { // Test RPC connection const blockchainInfo = await this.rpcCall('getblockchaininfo'); if (!blockchainInfo) { console.error('āŒ Failed to connect to RIN node!'); return; } console.log('āœ… Connected to RIN node'); console.log(`šŸ“Š Current height: ${blockchainInfo.blocks || 'unknown'}`); console.log(`šŸ”— Chain: ${blockchainInfo.chain || 'unknown'}`); // Get initial block template if (!(await this.getBlockTemplate())) { console.error('āŒ Failed to get initial block template!'); return; } // Start job updater this.startJobUpdater(); // Create TCP server this.server = net.createServer((client) => { const addr = `${client.remoteAddress}:${client.remotePort}`; this.handleClient(client, addr); }); // Start server this.server.listen(CONFIG.stratum.port, CONFIG.stratum.host, () => { this.running = true; const timestamp = new Date().toISOString(); console.log(`[${timestamp}] šŸš€ RIN Stratum Proxy ready!`); console.log(` šŸ“” Listening on ${CONFIG.stratum.host}:${CONFIG.stratum.port}`); console.log(` šŸ’° Mining to: ${CONFIG.mining.targetAddress}`); console.log(` šŸ“Š Current job: ${this.currentJob ? this.currentJob.jobId : 'None'}`); console.log(''); console.log(' šŸ”§ Miner command:'); console.log(` ./cpuminer -a rinhash -o stratum+tcp://${CONFIG.stratum.host}:${CONFIG.stratum.port} -u worker1 -p x -t 4`); console.log(''); }); this.server.on('error', (error) => { console.error(`āŒ Server error: ${error.message}`); }); } catch (error) { console.error(`āŒ Failed to start server: ${error.message}`); } } /** * Stop the server */ stop() { this.running = false; if (this.server) { this.server.close(); } console.log('šŸ›‘ Server stopped'); } } // Handle graceful shutdown process.on('SIGINT', () => { console.log('\nšŸ›‘ Shutting down...'); if (global.proxy) { global.proxy.stop(); } process.exit(0); }); // Start the proxy const proxy = new RinStratumProxy(); global.proxy = proxy; proxy.start().catch(error => { console.error(`āŒ Failed to start proxy: ${error.message}`); process.exit(1); });