From 9d4a1dda7f19af467f10a91927167a79b99164ca Mon Sep 17 00:00:00 2001 From: Ben Wilson Date: Sun, 25 Jun 2023 09:16:05 -0400 Subject: [PATCH] big refactor --- src/BlockTemplateService.ts | 5 +- src/models/MiningJob.ts | 157 ++++++++++++------ src/models/StratumV1Client.ts | 108 ++++-------- .../stratum-messages/MiningSubmitMessage.ts | 83 --------- 4 files changed, 145 insertions(+), 208 deletions(-) diff --git a/src/BlockTemplateService.ts b/src/BlockTemplateService.ts index 13e9868..5e17da3 100644 --- a/src/BlockTemplateService.ts +++ b/src/BlockTemplateService.ts @@ -3,19 +3,18 @@ import { from, map, Observable, shareReplay, switchMap, tap } from 'rxjs'; import { BitcoinRpcService } from './bitcoin-rpc.service'; import { IBlockTemplate } from './models/bitcoin-rpc/IBlockTemplate'; -import { IMiningInfo } from './models/bitcoin-rpc/IMiningInfo'; @Injectable() export class BlockTemplateService { public currentBlockTemplate: IBlockTemplate; - public currentBlockTemplate$: Observable<{ miningInfo: IMiningInfo, blockTemplate: IBlockTemplate }>; + public currentBlockTemplate$: Observable<{ blockTemplate: IBlockTemplate }>; constructor(private readonly bitcoinRpcService: BitcoinRpcService) { this.currentBlockTemplate$ = this.bitcoinRpcService.newBlock$.pipe( switchMap((miningInfo) => from(this.bitcoinRpcService.getBlockTemplate()).pipe(map(blockTemplate => { return { miningInfo, blockTemplate } }))), - tap(({ miningInfo, blockTemplate }) => this.currentBlockTemplate = blockTemplate), + tap(({ blockTemplate }) => this.currentBlockTemplate = blockTemplate), shareReplay({ refCount: true, bufferSize: 1 }) ); } diff --git a/src/models/MiningJob.ts b/src/models/MiningJob.ts index 22a850e..da4ccc7 100644 --- a/src/models/MiningJob.ts +++ b/src/models/MiningJob.ts @@ -11,43 +11,32 @@ interface AddressObject { } export class MiningJob { + private merkle_branch: string[]; // List of hashes, will be used for calculation of merkle root. This is not a list of all transactions, it only contains prepared hashes of steps of merkle tree algorithm. + public jobId: 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 coinb1: string; // The hex-encoded prefix of the coinbase transaction (to precede extra nonce 2). - public coinb2: string; //The hex-encoded suffix of the coinbase transaction (to follow extra nonce 2). - public merkle_branch: string[]; // List of hashes, will be used for calculation of merkle root. This is not a list of all transactions, it only contains prepared hashes of steps of merkle tree algorithm. - public version: number; // The hex-encoded block version. - public nbits: number; // The hex-encoded network difficulty required for the block. - public ntime: number; // Current ntime/ - public response: string; - - - private tree: MerkleTree; - - public coinbaseTransaction: bitcoinjs.Transaction; - public block: bitcoinjs.Block = new bitcoinjs.Block(); + public networkDifficulty: number; - constructor(id: string, payoutInformation: AddressObject[], public blockTemplate: IBlockTemplate, public networkDifficulty: number, public clean_jobs: boolean) { - + constructor(id: string, payoutInformation: AddressObject[], public blockTemplate: IBlockTemplate, public clean_jobs: boolean) { this.jobId = id; //this.target = blockTemplate.target; - this.prevhash = this.convertToLittleEndian(blockTemplate.previousblockhash); + this.block.prevHash = this.convertToLittleEndian(blockTemplate.previousblockhash); - this.version = blockTemplate.version; - this.nbits = parseInt(blockTemplate.bits, 16); - this.ntime = Math.floor(new Date().getTime() / 1000); + this.block.version = blockTemplate.version; + this.block.bits = parseInt(blockTemplate.bits, 16); + this.networkDifficulty = this.calculateNetworkDifficulty(this.block.bits); + this.block.timestamp = Math.floor(new Date().getTime() / 1000); + this.block.transactions = blockTemplate.transactions.map(t => bitcoinjs.Transaction.fromHex(t.data)); - const allTransactions = blockTemplate.transactions.map(t => bitcoinjs.Transaction.fromHex(t.data)); - - this.coinbaseTransaction = this.createCoinbaseTransaction(payoutInformation, this.blockTemplate.height, this.blockTemplate.coinbasevalue); - allTransactions.unshift(this.coinbaseTransaction); - const witnessRootHash = bitcoinjs.Block.calculateMerkleRoot(allTransactions, true); + const coinbaseTransaction = this.createCoinbaseTransaction(payoutInformation, this.blockTemplate.height, this.blockTemplate.coinbasevalue); + this.block.transactions.unshift(coinbaseTransaction); + this.block.witnessCommit = bitcoinjs.Block.calculateMerkleRoot(this.block.transactions, true); + this.block.merkleRoot = bitcoinjs.Block.calculateMerkleRoot(this.block.transactions, false); //The commitment is recorded in a scriptPubKey of the coinbase transaction. It must be at least 38 bytes, with the first 6-byte of 0x6a24aa21a9ed, that is: // 1-byte - OP_RETURN (0x6a) @@ -55,34 +44,82 @@ export class MiningJob { // 4-byte - Commitment header (0xaa21a9ed) const segwitMagicBits = Buffer.from('aa21a9ed', 'hex'); // 32-byte - Commitment hash: Double-SHA256(witness root hash|witness reserved value) - const commitmentHash = this.sha256(this.sha256(witnessRootHash)); + const commitmentHash = this.sha256(this.sha256(this.block.witnessCommit)); // 39th byte onwards: Optional data with no consensus meaning - this.coinbaseTransaction.outs[0].script = bitcoinjs.script.compile([bitcoinjs.opcodes.OP_RETURN, Buffer.concat([segwitMagicBits, commitmentHash])]); + coinbaseTransaction.outs[0].script = bitcoinjs.script.compile([bitcoinjs.opcodes.OP_RETURN, Buffer.concat([segwitMagicBits, commitmentHash])]); + // get the non-witness coinbase tx //@ts-ignore - const serializedTx = this.coinbaseTransaction.__toBuffer().toString('hex'); + const serializedCoinbaseTx = coinbaseTransaction.__toBuffer().toString('hex'); const blockHeightScript = `03${this.blockTemplate.height.toString(16).padStart(8, '0')}` + '00000000' + '00000000'; - const partOneIndex = serializedTx.indexOf(blockHeightScript) + blockHeightScript.length; + const partOneIndex = serializedCoinbaseTx.indexOf(blockHeightScript) + blockHeightScript.length; - const coinbasePart1 = serializedTx.slice(0, partOneIndex); - const coinbasePart2 = serializedTx.slice(partOneIndex); - this.coinb1 = coinbasePart1.slice(0, coinbasePart1.length - 16); - this.coinb2 = coinbasePart2; + const coinbasePart1 = serializedCoinbaseTx.slice(0, partOneIndex); + const coinbasePart2 = serializedCoinbaseTx.slice(partOneIndex); + const coinb1 = coinbasePart1.slice(0, coinbasePart1.length - 16); + const coinb2 = coinbasePart2; // Calculate merkle branch - const transactionBuffers = allTransactions.map(tx => tx.getHash(false)); + const transactionBuffers = this.block.transactions.map(tx => tx.getHash(false)); - this.tree = new MerkleTree(transactionBuffers, this.sha256, { isBitcoinTree: true }); + const tree = new MerkleTree(transactionBuffers, this.sha256, { isBitcoinTree: true }); + this.merkle_branch = tree.getProof(coinbaseTransaction.getHash(false)).map(p => p.data.toString('hex')); - this.merkle_branch = this.tree.getProof(this.coinbaseTransaction.getHash(false)).map(p => p.data.toString('hex')); + this.block.transactions[0] = coinbaseTransaction; - - this.constructResponse(); + this.constructResponse(coinb1, coinb2); } + public tryBlock(versionMaskString: number, nonce: number, extraNonce: string, extraNonce2: string): bitcoinjs.Block { + + const testBlock = bitcoinjs.Block.fromBuffer(this.block.toBuffer()); + + testBlock.nonce = nonce; + + // recompute version mask + const versionMask = versionMaskString; + if (versionMask !== undefined && versionMask != 0) { + testBlock.version = (testBlock.version ^ versionMask); + } + + // set the nonces + const blockHeightScript = `03${this.blockTemplate.height.toString(16).padStart(8, '0')}${extraNonce}${extraNonce2}`; + const inputScript = bitcoinjs.script.compile([bitcoinjs.opcodes.OP_RETURN, Buffer.from(blockHeightScript, 'hex')]); + testBlock.transactions[0].ins[0].script = inputScript; + + //@ts-ignore + // const test = testBlock.transactions[0].__toBuffer(); + // console.log(test.toString('hex')) + + const newRoot = this.calculateMerkleRootHash(testBlock.transactions[0].__toBuffer(), this.merkle_branch); + //recompute the root + testBlock.merkleRoot = newRoot; + + return testBlock; + } + + private calculateMerkleRootHash(coinbaseTx: string, merkleBranches: string[]): Buffer { + + let coinbaseTxBuf = Buffer.from(coinbaseTx, 'hex'); + + const bothMerkles = Buffer.alloc(64); + let test = this.sha256(coinbaseTxBuf) + let newRoot = this.sha256(test); + bothMerkles.set(newRoot); + + for (let i = 0; i < merkleBranches.length; i++) { + bothMerkles.set(Buffer.from(merkleBranches[i], 'hex'), 32); + newRoot = this.sha256(this.sha256(bothMerkles)); + bothMerkles.set(newRoot); + } + + return bothMerkles.subarray(0, 32) + } + + private createCoinbaseTransaction(addresses: AddressObject[], blockHeight: number, reward: number): bitcoinjs.Transaction { // Part 1 const coinbaseTransaction = new bitcoinjs.Transaction(); @@ -107,10 +144,11 @@ export class MiningJob { coinbaseTransaction.addOutput(scriptPubKey.output, amount); }) + //Add any remaining sats from the Math.floor + coinbaseTransaction.outs[0].value += rewardBalance; const segwitWitnessReservedValue = Buffer.alloc(32, 0); - //and the coinbase's input's witness must consist of a single 32-byte array for the witness reserved value coinbaseTransaction.ins[0].witness = [segwitWitnessReservedValue]; @@ -122,36 +160,57 @@ export class MiningJob { } - private constructResponse() { + private constructResponse(coinb1: string, coinb2: string) { const job = { id: null, method: eResponseMethod.MINING_NOTIFY, params: [ this.jobId, - this.prevhash, - this.coinb1, - this.coinb2, + this.swapEndianWords(this.block.prevHash).toString('hex'), + coinb1, + coinb2, this.merkle_branch, - this.version.toString(16), - this.nbits.toString(16), - this.ntime.toString(16), + this.block.version.toString(16), + this.block.bits.toString(16), + this.block.timestamp.toString(16), this.clean_jobs ] }; this.response = JSON.stringify(job); - } - private convertToLittleEndian(hash: string): string { + private convertToLittleEndian(hash: string): Buffer { const bytes = Buffer.from(hash, 'hex'); Array.prototype.reverse.call(bytes); - return bytes.toString('hex'); + return bytes; + } + + private swapEndianWords(buffer: Buffer): Buffer { + const swappedBuffer = Buffer.alloc(buffer.length); + + for (let i = 0; i < buffer.length; i += 4) { + swappedBuffer[i] = buffer[i + 3]; + swappedBuffer[i + 1] = buffer[i + 2]; + swappedBuffer[i + 2] = buffer[i + 1]; + swappedBuffer[i + 3] = buffer[i]; + } + + return swappedBuffer; } + private calculateNetworkDifficulty(nBits: number) { + const mantissa: number = nBits & 0x007fffff; // Extract the mantissa from nBits + const exponent: number = (nBits >> 24) & 0xff; // Extract the exponent from nBits + const target: number = mantissa * Math.pow(256, (exponent - 3)); // Calculate the target value + + const difficulty: number = (Math.pow(2, 208) * 65535) / target; // Calculate the difficulty + + return difficulty; + } } \ No newline at end of file diff --git a/src/models/StratumV1Client.ts b/src/models/StratumV1Client.ts index 51e9555..8cbf8cc 100644 --- a/src/models/StratumV1Client.ts +++ b/src/models/StratumV1Client.ts @@ -1,5 +1,4 @@ -import { Block, Transaction } from 'bitcoinjs-lib'; -import * as bitcoinjs from 'bitcoinjs-lib'; +import Big from 'big.js'; import { plainToInstance } from 'class-transformer'; import { validate, ValidatorOptions } from 'class-validator'; import * as crypto from 'crypto'; @@ -25,26 +24,17 @@ import { StratumV1ClientStatistics } from './StratumV1ClientStatistics'; export class StratumV1Client extends EasyUnsubscribe { - public startTime: Date; + private clientSubscription: SubscriptionMessage; + private clientConfiguration: ConfigurationMessage; + private clientAuthorization: AuthorizationMessage; + private clientSuggestedDifficulty: SuggestDifficulty; - public clientSubscription: SubscriptionMessage; - public clientConfiguration: ConfigurationMessage; - public clientAuthorization: AuthorizationMessage; - public clientSuggestedDifficulty: SuggestDifficulty; - - public statistics: StratumV1ClientStatistics; + private statistics: StratumV1ClientStatistics; + private stratumInitialized = false; + private clientDifficulty: number = 512; + private entity: ClientEntity; public extraNonce: string; - public stratumInitialized = false; - - public refreshInterval: NodeJS.Timer; - - public clientDifficulty: number = 512; - - public jobRefreshInterval: NodeJS.Timer; - - public entity: ClientEntity; - constructor( public readonly socket: Socket, @@ -55,7 +45,7 @@ export class StratumV1Client extends EasyUnsubscribe { private readonly clientStatisticsService: ClientStatisticsService ) { super(); - this.startTime = new Date(); + this.statistics = new StratumV1ClientStatistics(this.clientStatisticsService); this.extraNonce = this.getRandomHexString(); @@ -221,7 +211,6 @@ export class StratumV1Client extends EasyUnsubscribe { this.stratumInitialized = true; - this.entity = await this.clientService.save({ sessionId: this.extraNonce, address: this.clientAuthorization.address, @@ -229,26 +218,21 @@ export class StratumV1Client extends EasyUnsubscribe { startTime: new Date(), }); - - let lastIntervalCount = undefined; - combineLatest([this.blockTemplateService.currentBlockTemplate$, interval(60000).pipe(startWith(-1))]).subscribe(([{ miningInfo, blockTemplate }, interValCount]) => { + combineLatest([this.blockTemplateService.currentBlockTemplate$, interval(60000).pipe(startWith(-1))]).subscribe(([{ blockTemplate }, interValCount]) => { let clearJobs = false; if (lastIntervalCount === interValCount) { clearJobs = true; } lastIntervalCount = interValCount; - const job = new MiningJob(this.stratumV1JobsService.getNextId(), [{ address: this.clientAuthorization.address, percent: 100 }], blockTemplate, miningInfo.difficulty, clearJobs); + const job = new MiningJob(this.stratumV1JobsService.getNextId(), [{ address: this.clientAuthorization.address, percent: 100 }], blockTemplate, clearJobs); this.stratumV1JobsService.addJob(job, clearJobs); this.socket.write(job.response + '\n'); }) - - - } } @@ -260,74 +244,52 @@ export class StratumV1Client extends EasyUnsubscribe { if (job == null) { return; } - const diff = submission.calculateDifficulty(this.extraNonce, job, submission); + const updatedJobBlock = job.tryBlock( + parseInt(submission.versionMask, 16), + parseInt(submission.nonce, 16), + this.extraNonce, + submission.extraNonce2 + ); + const diff = this.calculateDifficulty(updatedJobBlock.toBuffer(true)); console.log(`DIFF: ${diff}`); if (diff >= this.clientDifficulty) { - const networkDifficulty = this.calculateNetworkDifficulty(parseInt(job.blockTemplate.bits, 16)); + await this.statistics.addSubmission(this.entity, this.clientDifficulty); if (diff > this.entity.bestDifficulty) { await this.clientService.updateBestDifficulty(this.extraNonce, diff); this.entity.bestDifficulty = diff; } - if (diff >= (networkDifficulty / 2)) { + if (diff >= (job.networkDifficulty / 2)) { console.log('!!! BOCK FOUND !!!'); - this.constructBlockAndBroadcast(job, submission); + const blockHex = updatedJobBlock.toHex(false); + this.bitcoinRpcService.SUBMIT_BLOCK(blockHex); } } else { console.log(`Difficulty too low`); } - } - private calculateNetworkDifficulty(nBits: number) { - const mantissa: number = nBits & 0x007fffff; // Extract the mantissa from nBits - const exponent: number = (nBits >> 24) & 0xff; // Extract the exponent from nBits + public calculateDifficulty(header: Buffer): number { - const target: number = mantissa * Math.pow(256, (exponent - 3)); // Calculate the target value + const hashBuffer: Buffer = crypto.createHash('sha256').update(header).digest(); + const hashResult: Buffer = crypto.createHash('sha256').update(hashBuffer).digest(); - const difficulty: number = (Math.pow(2, 208) * 65535) / target; // Calculate the difficulty + let s64 = this.le256todouble(hashResult); - return difficulty; + const truediffone = Big('26959535291011309493156476344723991336010898738574164086137773096960'); + return truediffone.div(s64.toString()).toNumber(); } - private constructBlockAndBroadcast(job: MiningJob, submission: MiningSubmitMessage) { - const block = new Block(); - const blockHeightScript = `03${job.blockTemplate.height.toString(16).padStart(8, '0')}${this.extraNonce}${submission.extraNonce2}`; + private le256todouble(target: Buffer): bigint { - const inputScript = bitcoinjs.script.compile([bitcoinjs.opcodes.OP_RETURN, Buffer.from(blockHeightScript, 'hex')]); - job.coinbaseTransaction.ins[0].script = inputScript; - - - const versionMask = parseInt(submission.versionMask, 16); - let version = job.version; - if (versionMask !== undefined && versionMask != 0) { - version = (version ^ versionMask); - } - - block.version = version; - block.prevHash = Buffer.from(job.prevhash, 'hex'); - - block.timestamp = job.ntime; - block.bits = job.nbits; - block.nonce = parseInt(submission.nonce, 16); - - block.transactions = job.blockTemplate.transactions.map(tx => { - return Transaction.fromHex(tx.data); - }); - block.transactions.unshift(job.coinbaseTransaction); - - block.merkleRoot = bitcoinjs.Block.calculateMerkleRoot(block.transactions, false); - block.witnessCommit = bitcoinjs.Block.calculateMerkleRoot(block.transactions, true); - - // const test1 = block.getWitnessCommit(); - // const test2 = block.checkTxRoots(); - - - const blockHex = block.toHex(false); - this.bitcoinRpcService.SUBMIT_BLOCK(blockHex); + const number = target.reduceRight((acc, byte) => { + // Shift the number 8 bits to the left and OR with the current byte + return (acc << BigInt(8)) | BigInt(byte); + }, BigInt(0)); + return number; } } \ No newline at end of file diff --git a/src/models/stratum-messages/MiningSubmitMessage.ts b/src/models/stratum-messages/MiningSubmitMessage.ts index d484ab8..2fbab6a 100644 --- a/src/models/stratum-messages/MiningSubmitMessage.ts +++ b/src/models/stratum-messages/MiningSubmitMessage.ts @@ -1,10 +1,7 @@ -import Big from 'big.js'; import { Expose, Transform } from 'class-transformer'; import { ArrayMaxSize, ArrayMinSize, IsArray, IsString } from 'class-validator'; -import * as crypto from 'crypto'; import { eRequestMethod } from '../enums/eRequestMethod'; -import { MiningJob } from '../MiningJob'; import { StratumBaseMessage } from './StratumBaseMessage'; export class MiningSubmitMessage extends StratumBaseMessage { @@ -65,88 +62,8 @@ export class MiningSubmitMessage extends StratumBaseMessage { } - public calculateDifficulty(clientId: string, job: MiningJob, submission: MiningSubmitMessage): number { - - const nonce = parseInt(submission.nonce, 16); - const versionMask = parseInt(submission.versionMask, 16); - const extraNonce = clientId; - const extraNonce2 = submission.extraNonce2; - - const coinbaseTx = `${job.coinb1}${extraNonce}${extraNonce2}${job.coinb2}`; - const newRoot = this.calculateMerkleRootHash(coinbaseTx, job.merkle_branch) - const truediffone = Big('26959535291011309493156476344723991336010898738574164086137773096960'); - - const header = Buffer.alloc(80); - - let version = job.version; - if (versionMask !== undefined && versionMask != 0) { - version = (version ^ versionMask); - } - - - header.writeUInt32LE(version, 0); - - header.write(this.swapEndianWords(job.prevhash), 4, 'hex') - newRoot.copy(header, 36, 0, 32) - header.writeUInt32LE(job.ntime, 68); - header.writeBigUint64LE(BigInt(job.nbits), 72); - header.writeUInt32LE(nonce, 76); - - const hashBuffer: Buffer = crypto.createHash('sha256').update(header).digest(); - const hashResult: Buffer = crypto.createHash('sha256').update(hashBuffer).digest(); - - - let s64 = this.le256todouble(hashResult); - - return truediffone.div(s64.toString()).toNumber(); - - - } - - private swapEndianWords(str: string) { - const hexGroups = str.match(/.{1,8}/g); - // Reverse each group and concatenate them - const reversedHexString = hexGroups.reduce((pre, cur, indx, arr) => { - const reversed = cur.match(/.{2}/g).reverse(); - return `${pre}${reversed.join('')}`; - }, ''); - return reversedHexString; - } - - - private le256todouble(target: Buffer): bigint { - - const number = target.reduceRight((acc, byte) => { - // Shift the number 8 bits to the left and OR with the current byte - return (acc << BigInt(8)) | BigInt(byte); - }, BigInt(0)); - - return number; - } - - private calculateMerkleRootHash(coinbaseTx: string, merkleBranches: string[]): Buffer { - - let coinbaseTxBuf = Buffer.from(coinbaseTx, 'hex'); - - const bothMerkles = Buffer.alloc(64); - let test = this.sha256(coinbaseTxBuf) - let newRoot = this.sha256(test); - bothMerkles.set(newRoot); - - for (let i = 0; i < merkleBranches.length; i++) { - bothMerkles.set(Buffer.from(merkleBranches[i], 'hex'), 32); - newRoot = this.sha256(this.sha256(bothMerkles)); - bothMerkles.set(newRoot); - } - - return bothMerkles.subarray(0, 32) - } - - private sha256(data: Buffer) { - return crypto.createHash('sha256').update(data).digest() - } } \ No newline at end of file