cleanup and change sewit witness output to its own tx

This commit is contained in:
Ben
2023-07-02 19:22:33 -04:00
parent b147eff49a
commit b1800419c4
13 changed files with 886 additions and 117 deletions
+14 -17
View File
@@ -12,17 +12,18 @@ interface AddressObject {
}
export class MiningJob {
private coinbasePart1: string;
private coinbasePart2: string;
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 response: string;
public block: bitcoinjs.Block = new bitcoinjs.Block();
public networkDifficulty: number;
constructor(id: string, payoutInformation: AddressObject[], public blockTemplate: IBlockTemplate, public clean_jobs: boolean) {
this.jobId = id;
//this.target = blockTemplate.target;
this.block.prevHash = this.convertToLittleEndian(blockTemplate.previousblockhash);
this.block.version = blockTemplate.version;
@@ -51,7 +52,8 @@ export class MiningJob {
// 32-byte - Commitment hash: Double-SHA256(witness root hash|witness reserved value)
const commitmentHash = this.sha256(this.sha256(this.block.witnessCommit));
// 39th byte onwards: Optional data with no consensus meaning
coinbaseTransaction.ins[0].script = bitcoinjs.script.compile([bitcoinjs.opcodes.OP_RETURN, Buffer.concat([blockHeightScript, segwitMagicBits, commitmentHash, Buffer.from('00000000' + '00000000', 'hex')])]);
coinbaseTransaction.ins[0].script = bitcoinjs.script.compile([bitcoinjs.opcodes.OP_RETURN, Buffer.concat([blockHeightScript, Buffer.from('00000000' + '00000000', 'hex')])]);
coinbaseTransaction.addOutput(bitcoinjs.script.compile([bitcoinjs.opcodes.OP_RETURN, Buffer.concat([segwitMagicBits, commitmentHash])]), 0);
// get the non-witness coinbase tx
//@ts-ignore
@@ -63,8 +65,8 @@ export class MiningJob {
const coinbasePart1 = serializedCoinbaseTx.slice(0, partOneIndex);
const coinbasePart2 = serializedCoinbaseTx.slice(partOneIndex);
const coinb1 = coinbasePart1.slice(0, coinbasePart1.length - 16);
const coinb2 = coinbasePart2;
this.coinbasePart1 = coinbasePart1.slice(0, coinbasePart1.length - 16);
this.coinbasePart2 = coinbasePart2;
// Calculate merkle branch
@@ -75,18 +77,15 @@ export class MiningJob {
this.block.transactions[0] = coinbaseTransaction;
this.constructResponse(coinb1, coinb2);
}
public copyAndUpdateBlock(versionMaskString: number, nonce: number, extraNonce: string, extraNonce2: string, timestamp: number): bitcoinjs.Block {
public copyAndUpdateBlock(versionMask: number, nonce: number, extraNonce: string, extraNonce2: string, timestamp: number): 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);
}
@@ -131,8 +130,6 @@ export class MiningJob {
// Set the version of the transaction
coinbaseTransaction.version = 2;
// Add the coinbase input (input with no previous output)
coinbaseTransaction.addInput(Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'), 0xffffffff, 0xffffffff);
@@ -145,7 +142,7 @@ export class MiningJob {
coinbaseTransaction.addOutput(this.getPaymentScript(recipientAddress.address), amount);
})
//Add any remaining sats from the Math.floor
//Add any remaining sats from the Math.floor
coinbaseTransaction.outs[0].value += rewardBalance;
const segwitWitnessReservedValue = Buffer.alloc(32, 0);
@@ -160,7 +157,7 @@ export class MiningJob {
const addressInfo = getAddressInfo(address);
switch (addressInfo.type) {
case AddressType.p2wpkh: {
return bitcoinjs.payments.p2wpkh({ address, network: bitcoinjs.networks.testnet }).output;
return bitcoinjs.payments.p2wpkh({ address }).output;
}
case AddressType.p2pkh: {
return bitcoinjs.payments.p2pkh({ address }).output;
@@ -185,7 +182,7 @@ export class MiningJob {
}
private constructResponse(coinb1: string, coinb2: string) {
public response(): string {
const job = {
id: null,
@@ -193,8 +190,8 @@ export class MiningJob {
params: [
this.jobId,
this.swapEndianWords(this.block.prevHash).toString('hex'),
coinb1,
coinb2,
this.coinbasePart1,
this.coinbasePart2,
this.merkle_branch,
this.block.version.toString(16),
this.block.bits.toString(16),
@@ -203,7 +200,7 @@ export class MiningJob {
]
};
this.response = JSON.stringify(job);
return JSON.stringify(job) + '\n';
}
+83 -50
View File
@@ -3,6 +3,7 @@ import { plainToInstance } from 'class-transformer';
import { validate, ValidatorOptions } from 'class-validator';
import * as crypto from 'crypto';
import { Socket } from 'net';
import PromiseSocket from 'promise-socket';
import { combineLatest, firstValueFrom, interval, startWith, takeUntil } from 'rxjs';
import { ClientStatisticsService } from '../ORM/client-statistics/client-statistics.service';
@@ -11,7 +12,7 @@ import { ClientService } from '../ORM/client/client.service';
import { BitcoinRpcService } from '../services/bitcoin-rpc.service';
import { BlockTemplateService } from '../services/block-template.service';
import { StratumV1JobsService } from '../services/stratum-v1-jobs.service';
import { EasyUnsubscribe } from '../utils/AutoUnsubscribe';
import { EasyUnsubscribe } from '../utils/EasyUnsubscribe';
import { IBlockTemplate } from './bitcoin-rpc/IBlockTemplate';
import { eRequestMethod } from './enums/eRequestMethod';
import { eResponseMethod } from './enums/eResponseMethod';
@@ -25,8 +26,6 @@ import { SubscriptionMessage } from './stratum-messages/SubscriptionMessage';
import { SuggestDifficulty } from './stratum-messages/SuggestDifficultyMessage';
import { StratumV1ClientStatistics } from './StratumV1ClientStatistics';
export class StratumV1Client extends EasyUnsubscribe {
private clientSubscription: SubscriptionMessage;
@@ -43,7 +42,7 @@ export class StratumV1Client extends EasyUnsubscribe {
public extraNonce: string;
constructor(
public readonly socket: Socket,
public readonly promiseSocket: PromiseSocket<Socket>,
private readonly stratumV1JobsService: StratumV1JobsService,
private readonly blockTemplateService: BlockTemplateService,
private readonly bitcoinRpcService: BitcoinRpcService,
@@ -57,7 +56,12 @@ export class StratumV1Client extends EasyUnsubscribe {
console.log(`New client ID: : ${this.extraNonce}`);
this.socket.on('data', this.handleData.bind(this, this.socket));
this.promiseSocket.socket.on('data', (data: Buffer) => {
data.toString()
.split('\n')
.filter(m => m.length > 0)
.forEach(m => this.handleMessage(m))
});
}
@@ -68,17 +72,8 @@ export class StratumV1Client extends EasyUnsubscribe {
return hexString;
}
private async handleData(socket: Socket, data: Buffer) {
const message = data.toString();
message.split('\n')
.filter(m => m.length > 0)
.forEach(this.handleMessage.bind(this, socket));
}
private async handleMessage(socket: Socket, message: string) {
private async handleMessage(message: string) {
console.log('Received:', message);
// Parse the message and check if it's the initial subscription message
@@ -87,7 +82,7 @@ export class StratumV1Client extends EasyUnsubscribe {
parsedMessage = JSON.parse(message);
} catch (e) {
console.log(e);
this.socket.end();
this.promiseSocket.end();
}
switch (parsedMessage.method) {
@@ -107,18 +102,15 @@ export class StratumV1Client extends EasyUnsubscribe {
if (errors.length === 0) {
this.clientSubscription = subscriptionMessage;
socket.write(JSON.stringify(this.clientSubscription.response(this.extraNonce)) + '\n');
await this.promiseSocket.write(JSON.stringify(this.clientSubscription.response(this.extraNonce)) + '\n');
} else {
const err = new StratumErrorMessage(
subscriptionMessage.id,
eStratumErrorCode.OtherUnknown,
'Subscription error',
errors).response();
console.error(err);
socket.write(err);
await this.promiseSocket.write(err);
}
break;
@@ -140,9 +132,15 @@ export class StratumV1Client extends EasyUnsubscribe {
if (errors.length === 0) {
this.clientConfiguration = configurationMessage;
//const response = this.buildSubscriptionResponse(configurationMessage.id);
socket.write(JSON.stringify(this.clientConfiguration.response()) + '\n');
await this.promiseSocket.write(JSON.stringify(this.clientConfiguration.response()) + '\n');
} else {
console.error(errors);
const err = new StratumErrorMessage(
configurationMessage.id,
eStratumErrorCode.OtherUnknown,
'Configuration error',
errors).response();
console.error(err);
await this.promiseSocket.write(err);
}
break;
@@ -164,9 +162,15 @@ export class StratumV1Client extends EasyUnsubscribe {
this.clientAuthorization = authorizationMessage;
//const response = this.buildSubscriptionResponse(authorizationMessage.id);
socket.write(JSON.stringify(this.clientAuthorization.response()) + '\n');
await this.promiseSocket.write(JSON.stringify(this.clientAuthorization.response()) + '\n');
} else {
console.error(errors);
const err = new StratumErrorMessage(
authorizationMessage.id,
eStratumErrorCode.OtherUnknown,
'Authorization error',
errors).response();
console.error(err);
await this.promiseSocket.write(err);
}
break;
@@ -192,10 +196,16 @@ export class StratumV1Client extends EasyUnsubscribe {
this.clientSuggestedDifficulty = suggestDifficultyMessage;
this.sessionDifficulty = suggestDifficultyMessage.suggestedDifficulty;
socket.write(JSON.stringify(this.clientSuggestedDifficulty.response(this.sessionDifficulty)) + '\n');
await this.promiseSocket.write(JSON.stringify(this.clientSuggestedDifficulty.response(this.sessionDifficulty)) + '\n');
this.usedSuggestedDifficulty = true;
} else {
console.error(errors);
const err = new StratumErrorMessage(
suggestDifficultyMessage.id,
eStratumErrorCode.OtherUnknown,
'Suggest difficulty error',
errors).response();
console.error(err);
await this.promiseSocket.write(err);
}
break;
}
@@ -214,11 +224,17 @@ export class StratumV1Client extends EasyUnsubscribe {
if (errors.length === 0) {
await this.handleMiningSubmission(miningSubmitMessage);
socket.write(JSON.stringify(miningSubmitMessage.response()) + '\n');
await this.promiseSocket.write(JSON.stringify(miningSubmitMessage.response()) + '\n');
} else {
console.error(errors);
const err = new StratumErrorMessage(
miningSubmitMessage.id,
eStratumErrorCode.OtherUnknown,
'Suggest difficulty error',
errors).response();
console.error(err);
await this.promiseSocket.write(err);
}
break;
}
@@ -233,7 +249,7 @@ export class StratumV1Client extends EasyUnsubscribe {
if (this.clientSuggestedDifficulty == null) {
console.log(`Setting difficulty to ${this.sessionDifficulty}`)
const setDifficulty = JSON.stringify(new SuggestDifficulty().response(this.sessionDifficulty));
this.socket.write(setDifficulty + '\n');
await this.promiseSocket.write(setDifficulty + '\n');
}
this.stratumInitialized = true;
@@ -264,7 +280,7 @@ export class StratumV1Client extends EasyUnsubscribe {
lastIntervalCount = interValCount;
this.sendNewMiningJob(blockTemplate, clearJobs);
await this.sendNewMiningJob(blockTemplate, clearJobs);
await this.checkDifficulty();
@@ -274,7 +290,7 @@ export class StratumV1Client extends EasyUnsubscribe {
}
}
private sendNewMiningJob(blockTemplate: IBlockTemplate, clearJobs: boolean) {
private async sendNewMiningJob(blockTemplate: IBlockTemplate, clearJobs: boolean) {
// const payoutInformation = [
// { address: 'bc1q99n3pu025yyu0jlywpmwzalyhm36tg5u37w20d', percent: 1.8 },
@@ -288,10 +304,9 @@ export class StratumV1Client extends EasyUnsubscribe {
const job = new MiningJob(this.stratumV1JobsService.getNextId(), payoutInformation, blockTemplate, clearJobs);
this.stratumV1JobsService.addJob(job, clearJobs);
;
const data = job.response + '\n';
this.socket.write(data);
await this.promiseSocket.write(job.response());
console.log(`Sent new job to ${this.extraNonce}. (clearJobs: ${clearJobs})`)
@@ -301,9 +316,14 @@ export class StratumV1Client extends EasyUnsubscribe {
private async handleMiningSubmission(submission: MiningSubmitMessage) {
const job = this.stratumV1JobsService.getJobById(submission.jobId);
// a miner may submit a job that doesn't exist anymore if it was removed by a new block notification
// a miner may submit a job that doesn't exist anymore if it was removed by a new block notification
if (job == null) {
console.log('job not found')
const err = new StratumErrorMessage(
submission.id,
eStratumErrorCode.JobNotFound,
'Job not found').response();
console.error(err);
await this.promiseSocket.write(err);
return;
}
const updatedJobBlock = job.copyAndUpdateBlock(
@@ -313,7 +333,7 @@ export class StratumV1Client extends EasyUnsubscribe {
submission.extraNonce2,
parseInt(submission.ntime, 16)
);
const submissionDifficulty = this.calculateDifficulty(updatedJobBlock.toBuffer(true));
const { submissionDifficulty, submissionHash } = this.calculateDifficulty(updatedJobBlock.toBuffer(true));
console.log(`DIFF: ${Math.round(submissionDifficulty)} of ${this.sessionDifficulty} from ${this.clientAuthorization.worker + '.' + this.extraNonce}`);
@@ -324,15 +344,30 @@ export class StratumV1Client extends EasyUnsubscribe {
const blockHex = updatedJobBlock.toHex(false);
this.bitcoinRpcService.SUBMIT_BLOCK(blockHex);
}
try {
await this.statistics.addSubmission(this.entity, submissionHash, this.sessionDifficulty);
} catch (e) {
const err = new StratumErrorMessage(
submission.id,
eStratumErrorCode.DuplicateShare,
'Duplicate share').response();
console.error(err);
await this.promiseSocket.write(err);
await this.statistics.addSubmission(this.entity, this.sessionDifficulty);
if (submissionDifficulty > this.entity.bestDifficulty) {
await this.clientService.updateBestDifficulty(this.extraNonce, submissionDifficulty);
this.entity.bestDifficulty = submissionDifficulty;
if (submissionDifficulty > this.entity.bestDifficulty) {
await this.clientService.updateBestDifficulty(this.extraNonce, submissionDifficulty);
this.entity.bestDifficulty = submissionDifficulty;
}
}
} else {
console.log(`Difficulty too low`);
const err = new StratumErrorMessage(
submission.id,
eStratumErrorCode.LowDifficultyShare,
'Difficulty too low').response();
console.error(err);
await this.promiseSocket.write(err);
}
await this.checkDifficulty();
@@ -355,19 +390,16 @@ export class StratumV1Client extends EasyUnsubscribe {
params: [targetDiff]
}) + '\n';
this.socket.write(data);
await this.promiseSocket.write(data);
// we need to clear the jobs so that the difficulty set takes effect. Otherwise the different miner implementations can cause issues
const { blockTemplate } = await firstValueFrom(this.blockTemplateService.currentBlockTemplate$);
this.sendNewMiningJob(blockTemplate, true);
await this.sendNewMiningJob(blockTemplate, true);
}
}
public calculateDifficulty(header: Buffer): number {
public calculateDifficulty(header: Buffer): { submissionDifficulty: number, submissionHash: string } {
const hashBuffer: Buffer = crypto.createHash('sha256').update(header).digest();
const hashResult: Buffer = crypto.createHash('sha256').update(hashBuffer).digest();
@@ -375,7 +407,8 @@ export class StratumV1Client extends EasyUnsubscribe {
let s64 = this.le256todouble(hashResult);
const truediffone = Big('26959535291011309493156476344723991336010898738574164086137773096960');
return truediffone.div(s64.toString()).toNumber();
const difficulty = truediffone.div(s64.toString()).toNumber();
return { submissionDifficulty: difficulty, submissionHash: hashResult.toString('hex') };
}
+5 -6
View File
@@ -12,7 +12,7 @@ export class StratumV1ClientStatistics {
this.submissionCacheStart = new Date();
}
public async addSubmission(client: ClientEntity, targetDifficulty: number) {
public async addSubmission(client: ClientEntity, submissionHash: string, targetDifficulty: number) {
if (this.submissionCache.length > CACHE_SIZE) {
this.submissionCache.shift();
@@ -29,6 +29,7 @@ export class StratumV1ClientStatistics {
address: client.address,
clientName: client.clientName,
sessionId: client.sessionId,
submissionHash
});
}
@@ -37,7 +38,7 @@ export class StratumV1ClientStatistics {
// miner hasn't submitted shares in one minute
if (this.submissionCache.length == 0 && (new Date().getTime() - this.submissionCacheStart.getTime()) / 1000 > 60) {
return this.blpo2(clientDifficulty >> 1);
return this.nearestPowerOfTwo(clientDifficulty >> 1);
}
if (this.submissionCache.length < CACHE_SIZE) {
@@ -55,15 +56,14 @@ export class StratumV1ClientStatistics {
const targetDifficulty = difficultyPerSecond * TARGET_SUBMISSION_PER_SECOND;
if (clientDifficulty << 1 < targetDifficulty || clientDifficulty >> 1 > targetDifficulty) {
return this.blpo2(targetDifficulty)
return this.nearestPowerOfTwo(targetDifficulty)
}
return null;
}
private blpo2(val) {
private nearestPowerOfTwo(val) {
let x = val | (val >> 1);
x = x | (x >> 2);
x = x | (x >> 4);
@@ -86,5 +86,4 @@ export class StratumV1ClientStatistics {
return res;
}
}
@@ -8,7 +8,7 @@ export class StratumErrorMessage {
private id: number = null,
private errorCode: eStratumErrorCode,
private errorMessage: string,
private validationErrors: ValidationError[]
private validationErrors: ValidationError[] = []
) {
}