diff --git a/.vscode/settings.json b/.vscode/settings.json index c5d6b4c..c59f322 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,7 @@ "nbits", "ntime", "prevhash", + "Satoshis", "submitblock", "Tempalte" ] diff --git a/src/bitcoin-stratum.provider.ts b/src/bitcoin-stratum.provider.ts index a094b39..8fd2581 100644 --- a/src/bitcoin-stratum.provider.ts +++ b/src/bitcoin-stratum.provider.ts @@ -18,25 +18,18 @@ export class BitcoinStratumProvider implements OnModuleInit { private interval: NodeJS.Timer; - private newMiningJobEmitter: BehaviorSubject = new BehaviorSubject(null); + private newMiningJobEmitter: BehaviorSubject = new BehaviorSubject(null); private latestJob: MiningJob; constructor(private readonly bitcoinRpcService: BitcoinRpcService) { - - - } async onModuleInit(): Promise { console.log('onModuleInit'); - this.blockTemplate = await this.bitcoinRpcService.getBlockTemplate(); - this.latestJob = new MiningJob(this.blockTemplate) - - this.server = new Server((socket: Socket) => { console.log('New client connected:', socket.remoteAddress); @@ -47,9 +40,9 @@ export class BitcoinStratumProvider implements OnModuleInit { if (this.latestJob == null) { return; } - const job = this.latestJob.response(); - const jobString = JSON.stringify(job); - client.localMiningJobEmitter.next(jobString); + + this.latestJob.constructResponse(); + client.localMiningJobEmitter.next(this.latestJob); }); // this.clients.push(client); @@ -59,24 +52,28 @@ export class BitcoinStratumProvider implements OnModuleInit { }); + this.bitcoinRpcService.newBlock().subscribe(async () => { + console.log('NEW BLOCK') + + this.blockTemplate = await this.bitcoinRpcService.getBlockTemplate(); + this.latestJob = new MiningJob(this.blockTemplate) + + this.latestJob.constructResponse(); + this.newMiningJobEmitter.next(this.latestJob); + clearInterval(this.interval); + this.interval = setInterval(async () => { + this.blockTemplate = await this.bitcoinRpcService.getBlockTemplate(); + + this.latestJob = new MiningJob(this.blockTemplate) + this.latestJob.constructResponse(); + this.newMiningJobEmitter.next(this.latestJob); + }, 60000); + }) + this.server.listen(3333, () => { console.log(`Bitcoin Stratum server is listening on port ${3333}`); }); - - - //clearInterval(this.interval); - - this.newMiningJobEmitter.next(JSON.stringify(this.latestJob.response())); - - this.interval = setInterval(async () => { - this.blockTemplate = await this.bitcoinRpcService.getBlockTemplate(); - - this.latestJob = new MiningJob(this.blockTemplate) - this.newMiningJobEmitter.next(JSON.stringify(this.latestJob.response())); - }, 60000); - - return; } diff --git a/src/models/MiningJob.ts b/src/models/MiningJob.ts index b9ea036..1e471f0 100644 --- a/src/models/MiningJob.ts +++ b/src/models/MiningJob.ts @@ -5,11 +5,17 @@ import { IBlockTemplate, IBlockTemplateTx } from './bitcoin-rpc/IBlockTemplate'; import { randomUUID } from 'crypto'; import { MerkleTree } from 'merkletreejs' +interface AddressObject { + address: string; + percentage: number; +} export class MiningJob { public id: number; public method: eResponseMethod.MINING_NOTIFY; 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 prevhash: string; // The hex-encoded previous block hash. @@ -21,9 +27,12 @@ export class MiningJob { 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 response: string; + constructor(blockTemplate: IBlockTemplate) { this.job_id = randomUUID(); + this.target = blockTemplate.target; this.prevhash = blockTemplate.previousblockhash; this.version = blockTemplate.version.toString(); @@ -39,7 +48,7 @@ export class MiningJob { console.log('TRANSACTION FEES', transactionFees); 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.coinb2 = coinbasePart2; @@ -62,6 +71,8 @@ export class MiningJob { this.merkle_branch = branch; + this.merkleRoot = tree.getRoot(); + } private calculateMiningReward(blockHeight: number): number { @@ -81,26 +92,43 @@ export class MiningJob { 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 const coinbaseScript = `03${blockHeight.toString(16).padStart(8, '0')}54696d652026204865616c74682021`; // Create coinbase transaction const version = '01000000'; - const inputs = '01' + '0000000000000000000000000000000000000000000000000000000000000000ffffffff'; + const inputCount = '01'; + const inputs = '0000000000000000000000000000000000000000000000000000000000000000ffffffff'; const coinbaseScriptSize = coinbaseScript.length / 2; const coinbaseScriptBytes = coinbaseScriptSize.toString(16).padStart(2, '0'); - const coinbaseTransaction = inputs + coinbaseScriptBytes + coinbaseScript + '00000000'; + const coinbaseTransaction = inputCount + inputs + coinbaseScriptBytes + coinbaseScript + '00000000'; - // Create output - const outputCount = '01'; - const satoshis = '0f4240'; // 6.25 BTC in satoshis (1 BTC = 100,000,000 satoshis) - const script = '1976a914' + address + '88ac'; // Change this to your desired output script - const locktime = '00000000'; + // Create outputs + const outputCount = addresses.length; + const outputCountHex = outputCount.toString(16).padStart(2, '0'); + + 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 - const coinbasePart1 = version + coinbaseTransaction + outputCount + satoshis; - const coinbasePart2 = script + locktime; + const coinbasePart1 = version + coinbaseTransaction + outputCountHex; + const coinbasePart2 = modifiedOutputs + '00000000'; return { coinbasePart1, coinbasePart2 }; } @@ -110,9 +138,9 @@ export class MiningJob { } - public response() { + public constructResponse() { - return { + const job = { id: 0, method: eResponseMethod.MINING_NOTIFY, params: [ @@ -126,7 +154,9 @@ export class MiningJob { this.ntime, this.clean_jobs ] - } + }; + + this.response = JSON.stringify(job); } diff --git a/src/models/StratumV1Client.ts b/src/models/StratumV1Client.ts index 63f8260..34009f9 100644 --- a/src/models/StratumV1Client.ts +++ b/src/models/StratumV1Client.ts @@ -10,6 +10,7 @@ import { ConfigurationMessage } from './stratum-messages/ConfigurationMessage'; import { MiningSubmitMessage } from './stratum-messages/MiningSubmitMessage'; import { SubscriptionMessage } from './stratum-messages/SubscriptionMessage'; import { SuggestDifficulty } from './stratum-messages/SuggestDifficultyMessage'; +import { MiningJob } from './MiningJob'; export class StratumV1Client extends EasyUnsubscribe { @@ -22,12 +23,14 @@ export class StratumV1Client extends EasyUnsubscribe { public onInitialized: BehaviorSubject = new BehaviorSubject(null); - public localMiningJobEmitter: BehaviorSubject = new BehaviorSubject(null); + public localMiningJobEmitter: BehaviorSubject = new BehaviorSubject(null); + private currentJob: MiningJob; + constructor( private readonly socket: Socket, - private readonly globalMiningJobEmitter: Observable + private readonly globalMiningJobEmitter: Observable ) { super(); @@ -44,11 +47,12 @@ export class StratumV1Client extends EasyUnsubscribe { 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) { 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) { //this.clientSuggestedDifficulty = miningSubmitMessage; + miningSubmitMessage.parse(); + this.handleMiningSubmission(miningSubmitMessage); socket.write(JSON.stringify(miningSubmitMessage.response()) + '\n'); } else { 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() { // const notification = { diff --git a/src/models/stratum-messages/MiningSubmitMessage.ts b/src/models/stratum-messages/MiningSubmitMessage.ts index 513c7ed..619b171 100644 --- a/src/models/stratum-messages/MiningSubmitMessage.ts +++ b/src/models/stratum-messages/MiningSubmitMessage.ts @@ -2,7 +2,10 @@ import { ArrayMaxSize, ArrayMinSize, IsArray } from 'class-validator'; import { eRequestMethod } from '../enums/eRequestMethod'; import { StratumBaseMessage } from './StratumBaseMessage'; +import { MiningJob } from '../MiningJob'; +import * as crypto from 'crypto'; +const trueDiffOne = Number('26959535291011309493156476344723991336010898738574164086137773096960'); export class MiningSubmitMessage extends StratumBaseMessage { @IsArray() @@ -10,11 +13,26 @@ export class MiningSubmitMessage extends StratumBaseMessage { @ArrayMaxSize(5) params: string[]; + public userId: string; + public jobId: string; + public extraNonce2: string; + public ntime: string; + public nonce: string + constructor() { super(); 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() { return { id: null, @@ -22,4 +40,45 @@ export class MiningSubmitMessage extends StratumBaseMessage { 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; + } } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index adb614c..fb27b86 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, - "target": "es2017", + "target": "ES2020", "sourceMap": true, "outDir": "./dist", "baseUrl": "./", @@ -18,4 +18,4 @@ "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false } -} +} \ No newline at end of file