From 29907cf323ef9fa5da5f86ee9d1e9f7d7095ee82 Mon Sep 17 00:00:00 2001 From: jayhines91 <81688708+jayhines91@users.noreply.github.com> Date: Fri, 13 Jun 2025 03:31:54 -0500 Subject: [PATCH 1/3] Implement AuxPoW proxy features --- README.md | 48 +++++++++++ bitcoin-elastos-proxy.js | 24 ++++++ lib/auxpow-builder.js | 37 +++++++++ lib/index.js | 4 + lib/mergedProxy.js | 168 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 281 insertions(+) create mode 100644 bitcoin-elastos-proxy.js create mode 100644 lib/auxpow-builder.js create mode 100644 lib/mergedProxy.js diff --git a/README.md b/README.md index 7592691d4..686bfaea5 100644 --- a/README.md +++ b/README.md @@ -343,6 +343,54 @@ Start pool pool.start(); ``` +### Merged Mining Proxy Example + +`bitcoin-elastos-proxy.js` demonstrates how to run a simple stratum proxy that +connects to an upstream Bitcoin pool and forwards work to miners while also +preparing auxiliary work for an Elastos daemon. The proxy accepts miners on the +configured port and handles share submissions by relaying them to the upstream +server. It automatically injects the current ELA aux block hash into each job +and, when a block is found, constructs the AuxPoW proof and submits it to the +Elastos daemon. + +```bash +node bitcoin-elastos-proxy.js +``` + +The library also exports an `auxpow` helper used internally to generate the +AuxPoW payload submitted to the Elastos daemon. + +### Integrating with U-NOMP + +To run the merged mining proxy inside a [U-NOMP](https://github.com/UNOMP/unified-node-open-mining-portal) deployment, +replace the bundled `node-stratum-pool` dependency with this repository and +launch the proxy alongside your pool instance. + +1. Clone U-NOMP and install its dependencies: + +```bash +git clone https://github.com/UNOMP/unified-node-open-mining-portal unomp +cd unomp +npm install +``` + +2. Replace the default stratum pool module with **node-merged-pool**: + +```bash +rm -rf node_modules/node-stratum-pool +git clone https://github.com/UNOMP/node-merged-pool node_modules/node-stratum-pool +``` + +3. Configure your coin's pool as usual. Use `bitcoin-elastos-proxy.js` as a +template for defining `upstream` and `elastosDaemon` settings. + +4. Start the proxy to accept miner connections: + +```bash +node bitcoin-elastos-proxy.js +``` + + Credits ------- diff --git a/bitcoin-elastos-proxy.js b/bitcoin-elastos-proxy.js new file mode 100644 index 000000000..7fabbb079 --- /dev/null +++ b/bitcoin-elastos-proxy.js @@ -0,0 +1,24 @@ +var Stratum = require('./lib/index.js'); + +var options = { + coin: { name: 'Bitcoin', symbol: 'BTC', algorithm: 'sha256' }, + ports: { '3333': { diff: 4 } }, + upstream: { host: '127.0.0.1', port: 3333, user: 'worker', password: 'x' }, + elastosDaemon: { + host: '127.0.0.1', + port: 21334, + user: 'user', + password: 'pass' + } +}; + +var proxy = Stratum.proxy.createProxy(options, function(ip, port, worker, pass, cb){ + console.log('Authorize ' + worker + ' from ' + ip); + cb({error: null, authorized: true, disconnect: false}); +}); + +proxy.on('ready', function(){ + console.log('Upstream ready, proxy started'); +}); + +proxy.start(); diff --git a/lib/auxpow-builder.js b/lib/auxpow-builder.js new file mode 100644 index 000000000..22c1b0d2c --- /dev/null +++ b/lib/auxpow-builder.js @@ -0,0 +1,37 @@ +const util = require('./util'); + +/** + * Build an AuxPoW proof for submitauxblock. + * Options: + * - parentBlockHeader: hex string of 80 byte BTC header + * - coinbaseTx: hex string of the coinbase transaction + * - merkleBranch: array of hex strings from mining.notify + * - auxBlockHash: hash returned by getauxblock + * - chainId: aux chain id (defaults to 1) + */ +exports.build = function(opts){ + opts = opts || {}; + const header = Buffer.from(opts.parentBlockHeader, 'hex'); + const coinbase = Buffer.from(opts.coinbaseTx, 'hex'); + const branch = (opts.merkleBranch || []).map(function(h){ + return Buffer.from(h, 'hex'); + }); + const coinbaseProof = Buffer.concat([ + util.varIntBuffer(branch.length), + Buffer.concat(branch), + util.packInt32LE(0) + ]); + const branchProof = Buffer.concat([ + util.varIntBuffer(0), + util.packInt32LE(0) + ]); + const blockHash = util.reverseBuffer(util.sha256d(header)); + const auxpow = Buffer.concat([ + coinbase, + blockHash, + coinbaseProof, + branchProof, + header + ]); + return auxpow.toString('hex'); +}; diff --git a/lib/index.js b/lib/index.js index fd5b7bad8..3c7699473 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,9 +5,13 @@ var events = require('events'); require('./algoProperties.js'); var pool = require('./pool.js'); +var mergedProxy = require('./mergedProxy.js'); +var auxpow = require('./auxpow-builder.js'); exports.daemon = require('./daemon.js'); exports.varDiff = require('./varDiff.js'); +exports.proxy = mergedProxy; +exports.auxpow = auxpow; exports.createPool = function(poolOptions, authorizeFn){ diff --git a/lib/mergedProxy.js b/lib/mergedProxy.js new file mode 100644 index 000000000..e15a1190a --- /dev/null +++ b/lib/mergedProxy.js @@ -0,0 +1,168 @@ +const net = require('net'); +const events = require('events'); +const Stratum = require('./stratum.js'); +const daemon = require('./daemon.js'); +const util = require('./util.js'); +const AuxPoW = require('./auxpow-builder.js'); + +function MergedMiningProxy(options, authorizeFn){ + events.EventEmitter.call(this); + const _this = this; + this.options = options || {}; + authorizeFn = authorizeFn || function(ip, port, worker, password, cb){ + cb({error: null, authorized: true, disconnect: false}); + }; + + let upstreamSocket; + let upstreamBuffer = ''; + let extranonce1 = null; + let extranonce2Size = 4; + let requestId = 3; + let pendingSubmit = {}; + let currentAuxBlock = null; + const jobMap = {}; + + const elaDaemon = options.elastosDaemon ? new daemon.interface([options.elastosDaemon]) : null; + + const stratumServer = new Stratum.Server(options, authorizeFn); + + stratumServer.on('client.connected', function(client){ + client.on('subscription', function(params, cb){ + cb(null, extranonce1, extranonce2Size); + if(options.diff) client.sendDifficulty(options.diff); + }).on('submit', function(params, cb){ + const id = ++requestId; + pendingSubmit[id] = { cb: cb, params: params }; + const submitParams = [client.workerName, params.jobId, params.extraNonce2, params.nTime, params.nonce]; + sendUpstream({id: id, method:'mining.submit', params: submitParams}); + }); + }); + + function connectUpstream(){ + upstreamSocket = net.connect(options.upstream.port, options.upstream.host, function(){ + sendUpstream({id:1, method:'mining.subscribe', params:[]}); + sendUpstream({id:2, method:'mining.authorize', params:[options.upstream.user, options.upstream.password]}); + }); + upstreamSocket.setEncoding('utf8'); + upstreamSocket.on('data', handleUpstreamData); + upstreamSocket.on('error', function(err){ _this.emit('error', err); }); + } + + function handleUpstreamData(data){ + upstreamBuffer += data; + let lines = upstreamBuffer.split('\n'); + upstreamBuffer = lines.pop(); + lines.forEach(function(line){ + if(!line.trim()) return; + let msg; + try{ msg = JSON.parse(line); } catch(e){ return; } + processMessage(msg); + }); + } + + function processMessage(msg){ + if(msg.id && pendingSubmit[msg.id]){ + const p = pendingSubmit[msg.id]; + p.cb(msg.error, msg.result); + if(!msg.error && msg.result){ + const job = jobMap[p.params.jobId]; + if(job) submitAuxPow(job, p.params); + } + delete pendingSubmit[msg.id]; + return; + } + if(msg.id === 1 && msg.result){ + extranonce1 = msg.result[1]; + extranonce2Size = msg.result[2]; + _this.emit('ready'); + return; + } + if(msg.method === 'mining.notify'){ + fetchAuxBlock(function(aux){ + const params = msg.params.slice(); + if(aux && aux.hash){ + const tag = 'fabe6d6d' + util.reverseHex(aux.hash) + '01000000' + '00000000'; + params[2] = params[2] + tag; + jobMap[params[0]] = { + jobId: params[0], + prevHash: params[1], + coinb1: params[2], + coinb2: params[3], + merkle_branch: params[4], + version: params[5], + nbits: params[6], + auxBlock: aux + }; + } + stratumServer.broadcastMiningJobs(params); + }); + return; + } + if(msg.method === 'mining.set_difficulty'){ + const diff = msg.params[0]; + const clients = stratumServer.getStratumClients(); + Object.keys(clients).forEach(function(id){ + clients[id].sendDifficulty(diff); + }); + return; + } + } + + function sendUpstream(obj){ + upstreamSocket.write(JSON.stringify(obj)+'\n'); + } + + function fetchAuxBlock(cb){ + if(!elaDaemon){ cb(); return; } + elaDaemon.cmd('getauxblock', [], function(result){ + const res = Array.isArray(result) ? result[0] : result; + if(res && res.response){ + currentAuxBlock = res.response; + } + cb(currentAuxBlock); + }); + } + + function serializeHeader(version, prevhash, merkleRoot, nTime, nBits, nonce){ + const header = Buffer.alloc(80); + let pos = 0; + header.write(nonce, pos, 4, 'hex'); + header.write(nBits, pos += 4, 4, 'hex'); + header.write(nTime, pos += 4, 4, 'hex'); + header.write(merkleRoot, pos += 4, 32, 'hex'); + header.write(prevhash, pos += 32, 32, 'hex'); + header.writeUInt32BE(parseInt(version, 16), pos + 32); + return util.reverseBuffer(header); + } + + function submitAuxPow(job, submit){ + if(!elaDaemon || !job || !currentAuxBlock) return; + const coinbaseHex = job.coinb1 + extranonce1 + submit.extraNonce2 + job.coinb2; + const coinbaseHash = util.sha256d(Buffer.from(coinbaseHex, 'hex')); + let root = coinbaseHash; + job.merkle_branch.forEach(function(step){ + root = util.sha256d(Buffer.concat([root, Buffer.from(step, 'hex')])); + }); + const merkleRootHex = util.reverseBuffer(root).toString('hex'); + const header = serializeHeader(job.version, job.prevHash, merkleRootHex, submit.nTime, job.nbits, submit.nonce); + const auxpowHex = AuxPoW.build({ + parentBlockHeader: header.toString('hex'), + coinbaseTx: coinbaseHex, + merkleBranch: job.merkle_branch, + auxBlockHash: job.auxBlock.hash, + chainId: 1 + }); + elaDaemon.cmd('submitauxblock', [job.auxBlock.hash, auxpowHex], function(){}, true); + } + + this.start = function(){ + connectUpstream(); + }; + + this.getStratumServer = function(){ return stratumServer; }; +} +MergedMiningProxy.prototype.__proto__ = events.EventEmitter.prototype; + +module.exports.createProxy = function(options, authorizeFn){ + return new MergedMiningProxy(options, authorizeFn); +}; From df474a60b4a80dfbb80914a4df29cf6880ddd6a3 Mon Sep 17 00:00:00 2001 From: jayhines91 <81688708+jayhines91@users.noreply.github.com> Date: Fri, 13 Jun 2025 04:04:21 -0500 Subject: [PATCH 2/3] Improve AuxPoW proxy and docs --- README.md | 8 ++++++++ lib/mergedProxy.js | 47 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 686bfaea5..5d5aba583 100644 --- a/README.md +++ b/README.md @@ -360,6 +360,14 @@ node bitcoin-elastos-proxy.js The library also exports an `auxpow` helper used internally to generate the AuxPoW payload submitted to the Elastos daemon. +### Elastos Daemon Setup + +The proxy expects an Elastos node with RPC enabled. Build the daemon from +the [Elastos.ELA repository](https://github.com/jayhines91/Elastos.ELA) and +launch it with `--rpcuser` and `--rpcpassword` configured. The proxy uses the +`createauxblock` and `submitauxblock` RPC calls as documented in that +repository. + ### Integrating with U-NOMP To run the merged mining proxy inside a [U-NOMP](https://github.com/UNOMP/unified-node-open-mining-portal) deployment, diff --git a/lib/mergedProxy.js b/lib/mergedProxy.js index e15a1190a..c283b2cc4 100644 --- a/lib/mergedProxy.js +++ b/lib/mergedProxy.js @@ -5,6 +5,25 @@ const daemon = require('./daemon.js'); const util = require('./util.js'); const AuxPoW = require('./auxpow-builder.js'); +function readVarInt(buf, offset){ + const tag = buf[offset]; + if(tag < 0xfd) return { value: tag, size: 1 }; + if(tag === 0xfd) return { value: buf.readUInt16LE(offset + 1), size: 3 }; + if(tag === 0xfe) return { value: buf.readUInt32LE(offset + 1), size: 5 }; + return { value: 0, size: 1 }; +} + +function updateScriptLen(coinb1Hex, extraBytes){ + const buf = Buffer.from(coinb1Hex, 'hex'); + const off = 4 + 1 + 32 + 4; + const v = readVarInt(buf, off); + const before = buf.slice(0, off); + const after = buf.slice(off + v.size); + const newLen = v.value + extraBytes; + const lenBuf = util.varIntBuffer(newLen); + return Buffer.concat([before, lenBuf, after]).toString('hex'); +} + function MergedMiningProxy(options, authorizeFn){ events.EventEmitter.call(this); const _this = this; @@ -20,6 +39,8 @@ function MergedMiningProxy(options, authorizeFn){ let requestId = 3; let pendingSubmit = {}; let currentAuxBlock = null; + let currentAuxTime = 0; + let lastPrevHash = null; const jobMap = {}; const elaDaemon = options.elastosDaemon ? new daemon.interface([options.elastosDaemon]) : null; @@ -78,14 +99,17 @@ function MergedMiningProxy(options, authorizeFn){ return; } if(msg.method === 'mining.notify'){ - fetchAuxBlock(function(aux){ + const prev = msg.params[1]; + fetchAuxBlock(prev !== lastPrevHash, function(aux){ + lastPrevHash = prev; const params = msg.params.slice(); if(aux && aux.hash){ const tag = 'fabe6d6d' + util.reverseHex(aux.hash) + '01000000' + '00000000'; - params[2] = params[2] + tag; + params[3] = params[3] + tag; + params[2] = updateScriptLen(params[2], tag.length / 2); jobMap[params[0]] = { jobId: params[0], - prevHash: params[1], + prevHash: prev, coinb1: params[2], coinb2: params[3], merkle_branch: params[4], @@ -112,12 +136,18 @@ function MergedMiningProxy(options, authorizeFn){ upstreamSocket.write(JSON.stringify(obj)+'\n'); } - function fetchAuxBlock(cb){ + function fetchAuxBlock(force, cb){ if(!elaDaemon){ cb(); return; } - elaDaemon.cmd('getauxblock', [], function(result){ + if(!force && currentAuxBlock && (Date.now() - currentAuxTime) < 60000){ + cb(currentAuxBlock); + return; + } + elaDaemon.cmd('createauxblock', [], function(result){ const res = Array.isArray(result) ? result[0] : result; if(res && res.response){ currentAuxBlock = res.response; + currentAuxTime = Date.now(); + console.log('[ELA] createauxblock', currentAuxBlock.hash); } cb(currentAuxBlock); }); @@ -152,7 +182,12 @@ function MergedMiningProxy(options, authorizeFn){ auxBlockHash: job.auxBlock.hash, chainId: 1 }); - elaDaemon.cmd('submitauxblock', [job.auxBlock.hash, auxpowHex], function(){}, true); + elaDaemon.cmd('submitauxblock', [job.auxBlock.hash, auxpowHex], function(res){ + console.log('[ELA] submitauxblock response:', res); + if(res && res.error){ + console.log('[ELA] auxpow hex', auxpowHex); + } + }, true); } this.start = function(){ From 561eb5beb9e2ebabda40ee6a8d3bd53eec8b1c24 Mon Sep 17 00:00:00 2001 From: jayhines91 <81688708+jayhines91@users.noreply.github.com> Date: Fri, 13 Jun 2025 04:21:24 -0500 Subject: [PATCH 3/3] Resolve leftover merge marker artifacts --- lib/mergedProxy.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/mergedProxy.js b/lib/mergedProxy.js index c283b2cc4..4993c6c7c 100644 --- a/lib/mergedProxy.js +++ b/lib/mergedProxy.js @@ -197,7 +197,6 @@ function MergedMiningProxy(options, authorizeFn){ this.getStratumServer = function(){ return stratumServer; }; } MergedMiningProxy.prototype.__proto__ = events.EventEmitter.prototype; - module.exports.createProxy = function(options, authorizeFn){ return new MergedMiningProxy(options, authorizeFn); };