Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,62 @@ 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.

### 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,
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
-------
Expand Down
24 changes: 24 additions & 0 deletions bitcoin-elastos-proxy.js
Original file line number Diff line number Diff line change
@@ -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();
37 changes: 37 additions & 0 deletions lib/auxpow-builder.js
Original file line number Diff line number Diff line change
@@ -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');
};
4 changes: 4 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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){
Expand Down
202 changes: 202 additions & 0 deletions lib/mergedProxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
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 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;
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;
let currentAuxTime = 0;
let lastPrevHash = 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'){
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[3] = params[3] + tag;
params[2] = updateScriptLen(params[2], tag.length / 2);
jobMap[params[0]] = {
jobId: params[0],
prevHash: prev,
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(force, cb){
if(!elaDaemon){ cb(); return; }
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);
});
}

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(res){
console.log('[ELA] submitauxblock response:', res);
if(res && res.error){
console.log('[ELA] auxpow hex', auxpowHex);
}
}, 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);
};