mining submit work

This commit is contained in:
Ben Wilson
2023-06-11 13:17:07 -04:00
parent 4ed46315b1
commit e8a5e263b0
6 changed files with 142 additions and 45 deletions
+1
View File
@@ -9,6 +9,7 @@
"nbits", "nbits",
"ntime", "ntime",
"prevhash", "prevhash",
"Satoshis",
"submitblock", "submitblock",
"Tempalte" "Tempalte"
] ]
+18 -21
View File
@@ -18,25 +18,18 @@ export class BitcoinStratumProvider implements OnModuleInit {
private interval: NodeJS.Timer; private interval: NodeJS.Timer;
private newMiningJobEmitter: BehaviorSubject<string> = new BehaviorSubject(null); private newMiningJobEmitter: BehaviorSubject<MiningJob> = new BehaviorSubject(null);
private latestJob: MiningJob; private latestJob: MiningJob;
constructor(private readonly bitcoinRpcService: BitcoinRpcService) { constructor(private readonly bitcoinRpcService: BitcoinRpcService) {
} }
async onModuleInit(): Promise<void> { async onModuleInit(): Promise<void> {
console.log('onModuleInit'); console.log('onModuleInit');
this.blockTemplate = await this.bitcoinRpcService.getBlockTemplate();
this.latestJob = new MiningJob(this.blockTemplate)
this.server = new Server((socket: Socket) => { this.server = new Server((socket: Socket) => {
console.log('New client connected:', socket.remoteAddress); console.log('New client connected:', socket.remoteAddress);
@@ -47,9 +40,9 @@ export class BitcoinStratumProvider implements OnModuleInit {
if (this.latestJob == null) { if (this.latestJob == null) {
return; return;
} }
const job = this.latestJob.response();
const jobString = JSON.stringify(job); this.latestJob.constructResponse();
client.localMiningJobEmitter.next(jobString); client.localMiningJobEmitter.next(this.latestJob);
}); });
// this.clients.push(client); // this.clients.push(client);
@@ -59,24 +52,28 @@ export class BitcoinStratumProvider implements OnModuleInit {
}); });
this.server.listen(3333, () => { this.bitcoinRpcService.newBlock().subscribe(async () => {
console.log(`Bitcoin Stratum server is listening on port ${3333}`); console.log('NEW BLOCK')
});
this.blockTemplate = await this.bitcoinRpcService.getBlockTemplate();
this.latestJob = new MiningJob(this.blockTemplate)
this.latestJob.constructResponse();
//clearInterval(this.interval); this.newMiningJobEmitter.next(this.latestJob);
clearInterval(this.interval);
this.newMiningJobEmitter.next(JSON.stringify(this.latestJob.response()));
this.interval = setInterval(async () => { this.interval = setInterval(async () => {
this.blockTemplate = await this.bitcoinRpcService.getBlockTemplate(); this.blockTemplate = await this.bitcoinRpcService.getBlockTemplate();
this.latestJob = new MiningJob(this.blockTemplate) this.latestJob = new MiningJob(this.blockTemplate)
this.newMiningJobEmitter.next(JSON.stringify(this.latestJob.response())); this.latestJob.constructResponse();
this.newMiningJobEmitter.next(this.latestJob);
}, 60000); }, 60000);
})
this.server.listen(3333, () => {
console.log(`Bitcoin Stratum server is listening on port ${3333}`);
});
return;
} }
+44 -14
View File
@@ -5,11 +5,17 @@ import { IBlockTemplate, IBlockTemplateTx } from './bitcoin-rpc/IBlockTemplate';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { MerkleTree } from 'merkletreejs' import { MerkleTree } from 'merkletreejs'
interface AddressObject {
address: string;
percentage: number;
}
export class MiningJob { export class MiningJob {
public id: number; public id: number;
public method: eResponseMethod.MINING_NOTIFY; public method: eResponseMethod.MINING_NOTIFY;
public params: string[]; public params: string[];
public target: string;
public merkleRoot: Buffer;
public job_id: string; // ID of the job. Use this ID while submitting share generated from this job. public job_id: string; // ID of the job. Use this ID while submitting share generated from this job.
public prevhash: string; // The hex-encoded previous block hash. public prevhash: string; // The hex-encoded previous block hash.
@@ -21,9 +27,12 @@ export class MiningJob {
public ntime: string; // Current ntime/ public ntime: string; // Current ntime/
public clean_jobs: boolean; // When true, server indicates that submitting shares from previous jobs don't have a sense and such shares will be rejected. When this flag is set, miner should also drop all previous jobs too. public clean_jobs: boolean; // When true, server indicates that submitting shares from previous jobs don't have a sense and such shares will be rejected. When this flag is set, miner should also drop all previous jobs too.
public response: string;
constructor(blockTemplate: IBlockTemplate) { constructor(blockTemplate: IBlockTemplate) {
this.job_id = randomUUID(); this.job_id = randomUUID();
this.target = blockTemplate.target;
this.prevhash = blockTemplate.previousblockhash; this.prevhash = blockTemplate.previousblockhash;
this.version = blockTemplate.version.toString(); this.version = blockTemplate.version.toString();
@@ -39,7 +48,7 @@ export class MiningJob {
console.log('TRANSACTION FEES', transactionFees); console.log('TRANSACTION FEES', transactionFees);
console.log('MINING REWARD', miningReward); console.log('MINING REWARD', miningReward);
const { coinbasePart1, coinbasePart2 } = this.createCoinbaseTransaction('', blockTemplate.height, transactionFees + miningReward); const { coinbasePart1, coinbasePart2 } = this.createCoinbaseTransaction([{ address: '', percentage: 100 }], blockTemplate.height, transactionFees + miningReward);
this.coinb1 = coinbasePart1; this.coinb1 = coinbasePart1;
this.coinb2 = coinbasePart2; this.coinb2 = coinbasePart2;
@@ -62,6 +71,8 @@ export class MiningJob {
this.merkle_branch = branch; this.merkle_branch = branch;
this.merkleRoot = tree.getRoot();
} }
private calculateMiningReward(blockHeight: number): number { private calculateMiningReward(blockHeight: number): number {
@@ -81,26 +92,43 @@ export class MiningJob {
return buffer.toString('hex'); return buffer.toString('hex');
} }
private createCoinbaseTransaction(address: string, blockHeight: number, reward: number): { coinbasePart1: string, coinbasePart2: string } {
private createCoinbaseTransaction(addresses: AddressObject[], blockHeight: number, reward: number): { coinbasePart1: string, coinbasePart2: string } {
// Generate coinbase script // Generate coinbase script
const coinbaseScript = `03${blockHeight.toString(16).padStart(8, '0')}54696d652026204865616c74682021`; const coinbaseScript = `03${blockHeight.toString(16).padStart(8, '0')}54696d652026204865616c74682021`;
// Create coinbase transaction // Create coinbase transaction
const version = '01000000'; const version = '01000000';
const inputs = '01' + '0000000000000000000000000000000000000000000000000000000000000000ffffffff'; const inputCount = '01';
const inputs = '0000000000000000000000000000000000000000000000000000000000000000ffffffff';
const coinbaseScriptSize = coinbaseScript.length / 2; const coinbaseScriptSize = coinbaseScript.length / 2;
const coinbaseScriptBytes = coinbaseScriptSize.toString(16).padStart(2, '0'); const coinbaseScriptBytes = coinbaseScriptSize.toString(16).padStart(2, '0');
const coinbaseTransaction = inputs + coinbaseScriptBytes + coinbaseScript + '00000000'; const coinbaseTransaction = inputCount + inputs + coinbaseScriptBytes + coinbaseScript + '00000000';
// Create output // Create outputs
const outputCount = '01'; const outputCount = addresses.length;
const satoshis = '0f4240'; // 6.25 BTC in satoshis (1 BTC = 100,000,000 satoshis) const outputCountHex = outputCount.toString(16).padStart(2, '0');
const script = '1976a914' + address + '88ac'; // Change this to your desired output script
const locktime = '00000000'; let remainingPayout = reward;
const outputs = addresses
.map((addressObj) => {
const percentage = addressObj.percentage / 100;
const satoshis = Math.floor(reward * percentage).toString(16).padStart(16, '0');
remainingPayout -= parseInt(satoshis, 16);
const script = '1976a914' + Buffer.from(addressObj.address, 'hex').toString('hex') + '88ac'; // Convert address to hex
return satoshis + script;
})
.join('');
// Distribute any remaining satoshis to the first address
const firstAddressSatoshis = (parseInt(outputs.substring(0, 16), 16) + remainingPayout).toString(16).padStart(16, '0');
const firstAddressOutput = firstAddressSatoshis + outputs.substring(16);
const modifiedOutputs = firstAddressOutput + outputs.slice(16);
// Combine coinbasePart1 and coinbasePart2 // Combine coinbasePart1 and coinbasePart2
const coinbasePart1 = version + coinbaseTransaction + outputCount + satoshis; const coinbasePart1 = version + coinbaseTransaction + outputCountHex;
const coinbasePart2 = script + locktime; const coinbasePart2 = modifiedOutputs + '00000000';
return { coinbasePart1, coinbasePart2 }; return { coinbasePart1, coinbasePart2 };
} }
@@ -110,9 +138,9 @@ export class MiningJob {
} }
public response() { public constructResponse() {
return { const job = {
id: 0, id: 0,
method: eResponseMethod.MINING_NOTIFY, method: eResponseMethod.MINING_NOTIFY,
params: [ params: [
@@ -126,7 +154,9 @@ export class MiningJob {
this.ntime, this.ntime,
this.clean_jobs this.clean_jobs
] ]
} };
this.response = JSON.stringify(job);
} }
+14 -4
View File
@@ -10,6 +10,7 @@ import { ConfigurationMessage } from './stratum-messages/ConfigurationMessage';
import { MiningSubmitMessage } from './stratum-messages/MiningSubmitMessage'; import { MiningSubmitMessage } from './stratum-messages/MiningSubmitMessage';
import { SubscriptionMessage } from './stratum-messages/SubscriptionMessage'; import { SubscriptionMessage } from './stratum-messages/SubscriptionMessage';
import { SuggestDifficulty } from './stratum-messages/SuggestDifficultyMessage'; import { SuggestDifficulty } from './stratum-messages/SuggestDifficultyMessage';
import { MiningJob } from './MiningJob';
export class StratumV1Client extends EasyUnsubscribe { export class StratumV1Client extends EasyUnsubscribe {
@@ -22,12 +23,14 @@ export class StratumV1Client extends EasyUnsubscribe {
public onInitialized: BehaviorSubject<void> = new BehaviorSubject(null); public onInitialized: BehaviorSubject<void> = new BehaviorSubject(null);
public localMiningJobEmitter: BehaviorSubject<string> = new BehaviorSubject(null); public localMiningJobEmitter: BehaviorSubject<MiningJob> = new BehaviorSubject(null);
private currentJob: MiningJob;
constructor( constructor(
private readonly socket: Socket, private readonly socket: Socket,
private readonly globalMiningJobEmitter: Observable<string> private readonly globalMiningJobEmitter: Observable<MiningJob>
) { ) {
super(); super();
@@ -44,11 +47,12 @@ export class StratumV1Client extends EasyUnsubscribe {
console.error('Socket error:', error); console.error('Socket error:', error);
}); });
merge(this.globalMiningJobEmitter, this.localMiningJobEmitter).pipe(takeUntil(this.easyUnsubscribe)).subscribe((job: string) => { merge(this.globalMiningJobEmitter, this.localMiningJobEmitter).pipe(takeUntil(this.easyUnsubscribe)).subscribe((job: MiningJob) => {
this.currentJob = job;
if (!this.initialized) { if (!this.initialized) {
return; return;
} }
this.socket.write(job + '\n'); this.socket.write(job.response + '\n');
}) })
@@ -185,6 +189,8 @@ export class StratumV1Client extends EasyUnsubscribe {
if (errors.length === 0) { if (errors.length === 0) {
//this.clientSuggestedDifficulty = miningSubmitMessage; //this.clientSuggestedDifficulty = miningSubmitMessage;
miningSubmitMessage.parse();
this.handleMiningSubmission(miningSubmitMessage);
socket.write(JSON.stringify(miningSubmitMessage.response()) + '\n'); socket.write(JSON.stringify(miningSubmitMessage.response()) + '\n');
} else { } else {
console.error(errors); console.error(errors);
@@ -209,6 +215,10 @@ export class StratumV1Client extends EasyUnsubscribe {
} }
private handleMiningSubmission(submission: MiningSubmitMessage) {
const diff = submission.testNonceValue(this.currentJob, submission.nonce);
console.log(diff);
}
// private miningNotify() { // private miningNotify() {
// const notification = { // const notification = {
@@ -2,7 +2,10 @@ import { ArrayMaxSize, ArrayMinSize, IsArray } from 'class-validator';
import { eRequestMethod } from '../enums/eRequestMethod'; import { eRequestMethod } from '../enums/eRequestMethod';
import { StratumBaseMessage } from './StratumBaseMessage'; import { StratumBaseMessage } from './StratumBaseMessage';
import { MiningJob } from '../MiningJob';
import * as crypto from 'crypto';
const trueDiffOne = Number('26959535291011309493156476344723991336010898738574164086137773096960');
export class MiningSubmitMessage extends StratumBaseMessage { export class MiningSubmitMessage extends StratumBaseMessage {
@IsArray() @IsArray()
@@ -10,11 +13,26 @@ export class MiningSubmitMessage extends StratumBaseMessage {
@ArrayMaxSize(5) @ArrayMaxSize(5)
params: string[]; params: string[];
public userId: string;
public jobId: string;
public extraNonce2: string;
public ntime: string;
public nonce: string
constructor() { constructor() {
super(); super();
this.method = eRequestMethod.AUTHORIZE; this.method = eRequestMethod.AUTHORIZE;
} }
public parse() {
this.userId = this.params[0];
this.jobId = this.params[1];
this.extraNonce2 = this.params[2];
this.ntime = this.params[3];
this.nonce = this.params[4];
}
public response() { public response() {
return { return {
id: null, id: null,
@@ -22,4 +40,45 @@ export class MiningSubmitMessage extends StratumBaseMessage {
result: true result: true
}; };
} }
testNonceValue(job: MiningJob, nonce: string): number {
const header: Buffer = Buffer.alloc(80);
// TODO: use the midstate hash instead of hashing the whole header
header.set(Buffer.from(job.version).subarray(0, 4), 0);
header.set(Buffer.from(job.prevhash).subarray(0, 32), 4);
header.set(Buffer.from(job.merkleRoot).subarray(0, 32), 36);
header.set(Buffer.from(job.ntime).subarray(0, 4), 68);
header.set(Buffer.from(job.target).subarray(0, 4), 72);
header.set(Buffer.from(nonce).subarray(0, 4), 76);
const hashBuffer = crypto.createHash('sha256').update(header).digest();
const hashResult = crypto.createHash('sha256').update(hashBuffer).digest();
const s64 = this.le256toDouble(hashResult);
const ds = trueDiffOne / s64;
console.log(trueDiffOne + '/ ' + s64)
return ds;
}
private le256toDouble(target: Buffer): number {
const bits192 = 6277101735386680763835789423207666416102355444464034512896n; // Replace with the actual value of bits192
const bits128 = 340282366920938463463374607431768211456n; // Replace with the actual value of bits128
const bits64 = 18446744073709551616n; // Replace with the actual value of bits64
const data64 = target.readBigUInt64LE(24);
let dcut64 = Number(data64) * Number(bits192);
dcut64 += Number(target.readBigUInt64LE(16)) * Number(bits128);
dcut64 += Number(target.readBigUInt64LE(8)) * Number(bits64);
dcut64 += Number(target.readBigUInt64LE(0));
return dcut64;
}
} }
+1 -1
View File
@@ -6,7 +6,7 @@
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"target": "es2017", "target": "ES2020",
"sourceMap": true, "sourceMap": true,
"outDir": "./dist", "outDir": "./dist",
"baseUrl": "./", "baseUrl": "./",