Files
public-pool/src/services/custom-work.service.ts
T
2026-06-22 10:40:01 -04:00

167 lines
5.8 KiB
TypeScript

/*
* Portions of this file are derived from or heavily informed by Blitzpool by warioishere.
* https://github.com/warioishere/blitzpool
*/
import { Injectable } from '@nestjs/common';
import { BufferReader } from '../models/sv2/sv2-binary-codec';
import { Sv2SetCustomMiningJob } from '../models/sv2/sv2-jdp-messages';
import { DifficultyUtils } from '../utils/difficulty.utils';
import { hash256 } from '../utils/hash.utils';
export interface CustomWorkCoinbaseSplit {
coinbasePrefix: Buffer;
coinbaseSuffix: Buffer;
}
export interface CustomWorkShareValidationInput {
coinbasePrefix: Buffer;
coinbaseSuffix: Buffer;
coinbaseTargetByteIndex?: number;
coinbaseTargetByte?: number;
merklePath: Buffer[];
extranoncePrefix: Buffer;
extranonce: Buffer;
prevHash: Buffer;
nBits: number;
version: number;
ntime: number;
nonce: number;
shareDifficulty: number;
networkDifficulty: number;
}
export interface CustomWorkShareValidationResult {
header: Buffer;
merkleRoot: Buffer;
hashBuffer: Buffer;
submissionDifficulty: number;
accepted: boolean;
isBlockCandidate: boolean;
}
@Injectable()
export class CustomWorkService {
public buildSv2CoinbaseSplit(job: Sv2SetCustomMiningJob, totalExtranonceSize: number): CustomWorkCoinbaseSplit {
const scriptSigLen = job.coinbasePrefix.length + totalExtranonceSize;
const scriptSigLenVarint = this.encodeBitcoinVarInt(scriptSigLen);
const txVersion = Buffer.alloc(4);
txVersion.writeUInt32LE(job.coinbaseTxVersion >>> 0, 0);
const nullOutpoint = Buffer.alloc(36);
nullOutpoint.writeUInt32LE(0xffffffff, 32);
const sequence = Buffer.alloc(4);
sequence.writeUInt32LE(job.coinbaseTxInputNSequence >>> 0, 0);
const locktime = Buffer.alloc(4);
locktime.writeUInt32LE(job.coinbaseTxLocktime >>> 0, 0);
return {
coinbasePrefix: Buffer.concat([
txVersion,
Buffer.from([0x01]),
nullOutpoint,
scriptSigLenVarint,
job.coinbasePrefix,
]),
coinbaseSuffix: Buffer.concat([
sequence,
job.coinbaseTxOutputs,
locktime,
]),
};
}
public validateShare(input: CustomWorkShareValidationInput): CustomWorkShareValidationResult {
const coinbaseTx = Buffer.concat([
input.coinbasePrefix,
input.extranoncePrefix,
input.extranonce,
input.coinbaseSuffix,
]);
if (input.coinbaseTargetByteIndex != null || input.coinbaseTargetByte != null) {
if (input.coinbaseTargetByteIndex == null || input.coinbaseTargetByte == null) {
throw new RangeError('Both coinbase target byte index and value are required');
}
if (input.coinbaseTargetByteIndex < 0 || input.coinbaseTargetByteIndex >= coinbaseTx.length) {
throw new RangeError(`Coinbase target byte index ${input.coinbaseTargetByteIndex} is outside coinbase length ${coinbaseTx.length}`);
}
coinbaseTx[input.coinbaseTargetByteIndex] = input.coinbaseTargetByte & 0xff;
}
const merkleRoot = this.computeMerkleRoot(coinbaseTx, input.merklePath);
const header = this.buildHeader(input.prevHash, merkleRoot, input.version, input.ntime, input.nBits, input.nonce);
const { submissionDifficulty, hashBuffer } = DifficultyUtils.calculateDifficulty(header);
return {
header,
merkleRoot,
hashBuffer,
submissionDifficulty,
accepted: DifficultyUtils.meetsTarget(hashBuffer, DifficultyUtils.difficultyToTarget(input.shareDifficulty)),
isBlockCandidate: DifficultyUtils.meetsTarget(hashBuffer, DifficultyUtils.difficultyToTarget(input.networkDifficulty)),
};
}
public computeMerkleRoot(coinbaseTx: Buffer, merklePath: Buffer[]): Buffer {
let merkleRoot = hash256(coinbaseTx);
const pair = Buffer.alloc(64);
for (const sibling of merklePath) {
pair.fill(0);
merkleRoot.copy(pair, 0);
sibling.copy(pair, 32);
merkleRoot = hash256(pair);
}
return merkleRoot;
}
public buildHeader(prevHash: Buffer, merkleRoot: Buffer, version: number, timestamp: number, nBits: number, nonce: number): Buffer {
const header = Buffer.alloc(80);
header.writeInt32LE(version, 0);
prevHash.copy(header, 4);
merkleRoot.copy(header, 36);
header.writeUInt32LE(timestamp >>> 0, 68);
header.writeUInt32LE(nBits >>> 0, 72);
header.writeUInt32LE(nonce >>> 0, 76);
return header;
}
public encodeBitcoinVarInt(value: number): Buffer {
if (!Number.isSafeInteger(value) || value < 0) {
throw new RangeError(`Invalid Bitcoin varint value ${value}`);
}
if (value < 0xfd) {
return Buffer.from([value]);
}
if (value <= 0xffff) {
const result = Buffer.alloc(3);
result[0] = 0xfd;
result.writeUInt16LE(value, 1);
return result;
}
if (value <= 0xffffffff) {
const result = Buffer.alloc(5);
result[0] = 0xfe;
result.writeUInt32LE(value, 1);
return result;
}
const result = Buffer.alloc(9);
result[0] = 0xff;
result.writeBigUInt64LE(BigInt(value), 1);
return result;
}
public readDatumNullTerminatedString(reader: BufferReader): string {
const bytes: number[] = [];
while (reader.remaining > 0) {
const byte = reader.readU8();
if (byte === 0) {
break;
}
bytes.push(byte);
}
return Buffer.from(bytes).toString('utf8');
}
}