Skip to content

Commit

Permalink
Move stratum code into its own directory for cleaner 'mining.configure'
Browse files Browse the repository at this point in the history
extensions in the future.

Add a 'mining.configure' handler.
Add a 'mining.configure' version-rolling extension handler.

Modify submitted blocks with miner-provided version bits when
version-rolling is negotiated successfully.
  • Loading branch information
rschifflin committed Aug 28, 2021
1 parent 54e4e35 commit 9d80e78
Show file tree
Hide file tree
Showing 7 changed files with 320 additions and 27 deletions.
17 changes: 10 additions & 7 deletions scripts/blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const util = require('./util.js');
const Merkle = require('./merkle.js');
const Transactions = require('./transactions.js');
const algorithms = require('./algorithms.js');
const versionRolling = require('./stratum/version_rolling');

// BlockTemplate Main Function
const BlockTemplate = function (jobId, rpcData, extraNoncePlaceholder, options) {
Expand Down Expand Up @@ -54,8 +55,8 @@ const BlockTemplate = function (jobId, rpcData, extraNoncePlaceholder, options)
}

// Push Submissions to Array
this.registerSubmit = function (extraNonce1, extraNonce2, nTime, nonce) {
const submission = extraNonce1 + extraNonce2 + nTime + nonce;
this.registerSubmit = function (extraNonce1, extraNonce2, nTime, nonce, versionRollingBits) {
const submission = extraNonce1 + extraNonce2 + nTime + nonce + versionRollingBits;
if (this.submits.indexOf(submission) === -1) {
this.submits.push(submission);
return true;
Expand Down Expand Up @@ -85,7 +86,7 @@ const BlockTemplate = function (jobId, rpcData, extraNoncePlaceholder, options)
};

// Serialize Block Headers
this.serializeHeader = function (merkleRoot, nTime, nonce, optionsArg) {
this.serializeHeader = function (merkleRoot, nTime, nonce, version, optionsArg) {
const headerBuf = Buffer.alloc(80);
let position = 0;
switch (optionsArg.coin.algorithm) {
Expand All @@ -95,7 +96,7 @@ const BlockTemplate = function (jobId, rpcData, extraNoncePlaceholder, options)
headerBuf.write(nTime, position += 4, 4, 'hex');
headerBuf.write(merkleRoot, position += 4, 32, 'hex');
headerBuf.write(this.rpcData.previousblockhash, position += 32, 32, 'hex');
headerBuf.writeUInt32BE(this.rpcData.version, position + 32);
headerBuf.writeUInt32BE(version, position + 32);
return util.reverseBuffer(headerBuf);
}
};
Expand All @@ -116,8 +117,10 @@ const BlockTemplate = function (jobId, rpcData, extraNoncePlaceholder, options)

// Serialize and return the crafted block header, and also return
// a continuation function for constructing the full solution to submit if desired
this.startSolution = function (coinbaseBuffer, merkleRoot, nTime, nonce, optionsArg) {
const headerBuffer = this.serializeHeader(merkleRoot, nTime, nonce, optionsArg);
this.startSolution = function (coinbaseBuffer, merkleRoot, nTime, nonce, versionRollingBits, optionsArg) {
const version = (this.rpcData.version & ~versionRolling.maxMaskBits) | versionRollingBits;
const headerBuffer = this.serializeHeader(merkleRoot, nTime, nonce, version, optionsArg);

const finishSolution = function () {
return this.serializeBlock(headerBuffer, coinbaseBuffer, optionsArg).toString('hex');
}.bind(this);
Expand All @@ -135,7 +138,7 @@ const BlockTemplate = function (jobId, rpcData, extraNoncePlaceholder, options)
this.generation[0].toString('hex'),
this.generation[1].toString('hex'),
this.merkleBranchHex,
util.packInt32BE(this.rpcData.version).toString('hex'),
util.packInt32BE(this.rpcData.version & ~versionRolling.maxMaskBits).toString('hex'),
this.rpcData.bits,
util.packUInt32BE(this.rpcData.curtime).toString('hex'),
true,
Expand Down
18 changes: 10 additions & 8 deletions scripts/candidates.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const bignum = require('bignum');
const util = require('./util.js');
const Transactions = require('./transactions.js');
const algorithms = require('./algorithms.js');
const versionRolling = require('./stratum/version_rolling');

// MiningTemplate Main Function
const MiningCandidate = function (jobId, rawRpcData, extraNoncePlaceholder, options) {
Expand Down Expand Up @@ -45,8 +46,8 @@ const MiningCandidate = function (jobId, rawRpcData, extraNoncePlaceholder, opti
this.prevHashReversed = util.reverseByteOrder(Buffer.from(rpcData.previousblockhash, 'hex')).toString('hex');

// Push Submissions to Array
this.registerSubmit = function (extraNonce1, extraNonce2, nTime, nonce) {
const submission = extraNonce1 + extraNonce2 + nTime + nonce;
this.registerSubmit = function (extraNonce1, extraNonce2, nTime, nonce, versionRollingBits) {
const submission = extraNonce1 + extraNonce2 + nTime + nonce + versionRollingBits;
if (this.submits.indexOf(submission) === -1) {
this.submits.push(submission);
return true;
Expand Down Expand Up @@ -76,7 +77,7 @@ const MiningCandidate = function (jobId, rawRpcData, extraNoncePlaceholder, opti
};

// Serialize Block Headers
this.serializeHeader = function (merkleRoot, nTime, nonce, optionsArg) {
this.serializeHeader = function (merkleRoot, nTime, nonce, version, optionsArg) {
const headerBuf = Buffer.alloc(80);
let position = 0;
switch (optionsArg.coin.algorithm) {
Expand All @@ -86,22 +87,23 @@ const MiningCandidate = function (jobId, rawRpcData, extraNoncePlaceholder, opti
headerBuf.write(nTime, position += 4, 4, 'hex');
headerBuf.write(merkleRoot, position += 4, 32, 'hex');
headerBuf.write(this.rpcData.previousblockhash, position += 32, 32, 'hex');
headerBuf.writeUInt32BE(this.rpcData.version, position + 32);
headerBuf.writeUInt32BE(version, position + 32);
return util.reverseBuffer(headerBuf);
}
};

// Serialize and return the crafted block header, and also return
// a continuation function for constructing the full solution to submit if desired
this.startSolution = function (coinbaseBuffer, merkleRoot, nTime, nonce, optionsArg) {
const headerBuffer = this.serializeHeader(merkleRoot, nTime, nonce, optionsArg);
this.startSolution = function (coinbaseBuffer, merkleRoot, nTime, nonce, versionRollingBits, optionsArg) {
const version = (this.rpcData.version & ~versionRolling.maxMaskBits) | versionRollingBits;
const headerBuffer = this.serializeHeader(merkleRoot, nTime, nonce, version, optionsArg);
const finishSolution = function () {
return {
id: this.rpcData.id,
nonce: parseInt(nonce, 16),
coinbase: coinbaseBuffer.toString('hex'),
time: parseInt(nTime, 16),
version: this.rpcData.version
version
};
}.bind(this);
return [headerBuffer, finishSolution];
Expand All @@ -118,7 +120,7 @@ const MiningCandidate = function (jobId, rawRpcData, extraNoncePlaceholder, opti
this.generation[0].toString('hex'),
this.generation[1].toString('hex'),
this.merkleBranchHex,
util.packInt32BE(this.rpcData.version).toString('hex'),
util.packInt32BE(this.rpcData.version & ~versionRolling.maskMaxBits).toString('hex'),
this.rpcData.bits,
util.packUInt32BE(this.rpcData.curtime).toString('hex'),
true,
Expand Down
6 changes: 3 additions & 3 deletions scripts/manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ const Manager = function (options) {

// Process New Submitted Share
this.processShare = function (jobId, previousPoolDifficulty, poolDifficulty, extraNonce1,
extraNonce2, nTime, nonce, ipAddress, port, workerName) {
extraNonce2, nTime, nonce, versionRollingBits, ipAddress, port, workerName) {
// Share is Invalid
const shareError = function (error) {
_this.emit('share', {
Expand Down Expand Up @@ -164,7 +164,7 @@ const Manager = function (options) {
if (nonce.length !== 8) {
return shareError([20, 'incorrect size of nonce']);
}
if (!job.registerSubmit(extraNonce1, extraNonce2, nTime, nonce)) {
if (!job.registerSubmit(extraNonce1, extraNonce2, nTime, nonce, versionRollingBits)) {
return shareError([22, 'duplicate share']);
}

Expand All @@ -182,7 +182,7 @@ const Manager = function (options) {
).toString('hex');

const [headerBuffer, finishSolution] = job.startSolution(
coinbaseBuffer, merkleRoot, nTime, nonce, options
coinbaseBuffer, merkleRoot, nTime, nonce, versionRollingBits, options
);
const headerHash = hashDigest(headerBuffer, nTimeInt);
const headerBigNum = bignum.fromBuffer(headerHash, { endian: 'little', size: 32 });
Expand Down
3 changes: 2 additions & 1 deletion scripts/pool.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const Difficulty = require('./difficulty.js');
const Daemon = require('./daemon.js');
const Manager = require('./manager.js');
const Peer = require('./peer.js');
const Stratum = require('./stratum.js');
const Stratum = require('./stratum');

// Pool Main Function
const Pool = function (initialOptions, authorizeFn) {
Expand Down Expand Up @@ -554,6 +554,7 @@ const Pool = function (initialOptions, authorizeFn) {
params.extraNonce2,
params.nTime,
params.nonce,
params.versionRollingBits,
client.remoteAddress,
client.socket.localPort,
params.name,
Expand Down
74 changes: 66 additions & 8 deletions scripts/stratum.js → scripts/stratum/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Import Required Modules
const net = require('net');
const events = require('events');
const util = require('./util.js');
const util = require('../util.js');
const versionRolling = require('./version_rolling.js');

// Increment Count for Each Subscription
const SubscriptionCounter = function () {
Expand Down Expand Up @@ -98,6 +99,36 @@ const StratumClient = function (options) {
return true;
};

function handleConfigure(message) {
const [extensions, extensionParams] = message.params;

const handleVersionRolling = (versionRollingParams) => {
const result = versionRolling.handle(versionRollingParams);
if (result['version-rolling']) {
_this.versionRollingMaskHex = result['version-rolling.mask'];
}
return result;
};

const handlers = { 'version-rolling': handleVersionRolling };

let response = {};
extensions.forEach((extension) => {
if (handlers[extension]) {
const extensionResult = handlers[extension](extensionParams);
response = { ...response, ...extensionResult };
} else {
response[extension] = false;
}
});

sendJson({
id: message.id,
result: response,
error: null,
});
}

// Manage Stratum Subscription
function handleSubscribe(message) {
if (!_this.authorized) {
Expand Down Expand Up @@ -168,14 +199,36 @@ const StratumClient = function (options) {
considerBan(false);
return;
}

const submitParams = {
name: message.params[0],
jobId: message.params[1],
extraNonce2: message.params[2],
nTime: message.params[3].toLowerCase(),
nonce: message.params[4].toLowerCase()
};

// If version rolling is enabled, and version bits are sent
// Validate the bits and add it to the submitParams
if (message.params[5] && _this.versionRollingMaskHex) {
if (!versionRolling.validate(message.params[5], _this.versionRollingMaskHex)) {
sendJson({
id: message.id,
result: null,
error: [20, 'invalid version rolling bits', null],
});
considerBan(false);
return;
}

const versionRollingBits = parseInt(message.params[5], 16);
submitParams.versionRollingBits = versionRollingBits;
} else {
submitParams.versionRollingBits = 0;
}

_this.emit('submit',
{
name: message.params[0],
jobId: message.params[1],
extraNonce2: message.params[2],
nTime: message.params[3].toLowerCase(),
nonce: message.params[4].toLowerCase(),
},
submitParams,
(error, result) => {
if (!considerBan(result)) {
sendJson({
Expand All @@ -190,6 +243,11 @@ const StratumClient = function (options) {
// Handle Stratum Messages
function handleMessage(message) {
switch (message.method) {
// Manage mining.configure
case 'mining.configure':
handleConfigure(message);
break;

// Manage Stratum Subscription
case 'mining.subscribe':
handleSubscribe(message);
Expand Down
52 changes: 52 additions & 0 deletions scripts/stratum/version_rolling.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const maxMaskHex = '1fffe000'; // See BIP-320
const maxMaskBits = parseInt(maxMaskHex, 16);

// Validates a 4-byte hex-encoded string
function validHex(hexChars) {
const hexAlphabet = '0123456789aAbBcCdDeEfF';
return (typeof hexChars === 'string')
&& (hexChars.length === 8)
&& ([...hexChars].every((hexChar) => hexAlphabet.includes(hexChar)));
}

function validate(bitsHex, maskHex) {
if (!validHex(bitsHex)) {
return false;
}

const bits = parseInt(bitsHex, 16);
const allowedBits = parseInt(maskHex, 16);
return (bits & ~allowedBits) === 0;
}

function handle(params) {
const response = {};
const requestMaskHex = params && params['version-rolling.mask'];

if (!requestMaskHex) {
response['version-rolling'] = true;
response['version-rolling.mask'] = maxMaskHex;
return response;
}

if (!validHex(requestMaskHex)) {
response['version-rolling'] = 'Invalid version-rolling.mask parameter.';
return response;
}

const requestMaskBits = parseInt(requestMaskHex, 16);
const responseMaskBits = requestMaskBits & maxMaskBits;
const responseMaskHex = responseMaskBits.toString(16).padStart(8, '0');

response['version-rolling'] = true;
response['version-rolling.mask'] = responseMaskHex;
return response;
}

module.exports = {
maxMaskHex,
maxMaskBits,
validHex,
validate,
handle
};
Loading

0 comments on commit 9d80e78

Please sign in to comment.