mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 09:05:06 -07:00
refactor project to use one singleton for blocks (memory optimization)
This commit is contained in:
Vendored
+1
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"coinb",
|
"coinb",
|
||||||
|
"coinbasevalue",
|
||||||
"Fastify",
|
"Fastify",
|
||||||
"getblocktemplate",
|
"getblocktemplate",
|
||||||
"getmininginfo",
|
"getmininginfo",
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { ClientStatisticsModule } from './ORM/client-statistics/client-statistic
|
|||||||
import { ClientModule } from './ORM/client/client.module';
|
import { ClientModule } from './ORM/client/client.module';
|
||||||
import { TelegramSubscriptionsModule } from './ORM/telegram-subscriptions/telegram-subscriptions.module';
|
import { TelegramSubscriptionsModule } from './ORM/telegram-subscriptions/telegram-subscriptions.module';
|
||||||
import { BitcoinRpcService } from './services/bitcoin-rpc.service';
|
import { BitcoinRpcService } from './services/bitcoin-rpc.service';
|
||||||
import { BlockTemplateService } from './services/block-template.service';
|
|
||||||
import { CleanupService } from './services/cleanup.service';
|
import { CleanupService } from './services/cleanup.service';
|
||||||
import { DiscordService } from './services/discord.service';
|
import { DiscordService } from './services/discord.service';
|
||||||
import { NotificationService } from './services/notification.service';
|
import { NotificationService } from './services/notification.service';
|
||||||
@@ -56,7 +55,6 @@ const ORMModules = [
|
|||||||
StratumV1Service,
|
StratumV1Service,
|
||||||
TelegramService,
|
TelegramService,
|
||||||
BitcoinRpcService,
|
BitcoinRpcService,
|
||||||
BlockTemplateService,
|
|
||||||
NotificationService,
|
NotificationService,
|
||||||
BitcoinAddressValidator
|
BitcoinAddressValidator
|
||||||
],
|
],
|
||||||
|
|||||||
+31
-66
@@ -1,51 +1,41 @@
|
|||||||
import { AddressType, getAddressInfo } from 'bitcoin-address-validation';
|
import { AddressType, getAddressInfo } from 'bitcoin-address-validation';
|
||||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||||
import * as merkle from 'merkle-lib';
|
|
||||||
import * as merkleProof from 'merkle-lib/proof';
|
|
||||||
|
|
||||||
import { IBlockTemplate } from './bitcoin-rpc/IBlockTemplate';
|
import { IJobTemplate } from '../services/stratum-v1-jobs.service';
|
||||||
import { eResponseMethod } from './enums/eResponseMethod';
|
import { eResponseMethod } from './enums/eResponseMethod';
|
||||||
|
|
||||||
|
|
||||||
interface AddressObject {
|
interface AddressObject {
|
||||||
address: string;
|
address: string;
|
||||||
percent: number;
|
percent: number;
|
||||||
}
|
}
|
||||||
export class MiningJob {
|
export class MiningJob {
|
||||||
|
|
||||||
|
private coinbaseTransaction: bitcoinjs.Transaction;
|
||||||
private coinbasePart1: string;
|
private coinbasePart1: string;
|
||||||
private coinbasePart2: 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.
|
// 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 jobId: string; // ID of the job. Use this ID while submitting share generated from this job.
|
||||||
public block: bitcoinjs.Block = new bitcoinjs.Block();
|
public jobTemplateId: string;
|
||||||
|
//public block: bitcoinjs.Block = new bitcoinjs.Block();
|
||||||
public networkDifficulty: number;
|
public networkDifficulty: number;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private network: bitcoinjs.networks.Network,
|
private network: bitcoinjs.networks.Network,
|
||||||
id: string,
|
id: string,
|
||||||
payoutInformation: AddressObject[],
|
payoutInformation: AddressObject[],
|
||||||
public blockTemplate: IBlockTemplate,
|
jobTemplate: IJobTemplate
|
||||||
public clean_jobs: boolean) {
|
) {
|
||||||
|
|
||||||
|
|
||||||
this.jobId = id;
|
this.jobId = id;
|
||||||
this.block.prevHash = this.convertToLittleEndian(blockTemplate.previousblockhash);
|
this.jobTemplateId = jobTemplate.blockData.id,
|
||||||
|
|
||||||
this.block.version = blockTemplate.version;
|
this.coinbaseTransaction = this.createCoinbaseTransaction(payoutInformation, jobTemplate.blockData.coinbasevalue);
|
||||||
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 coinbaseTransaction = this.createCoinbaseTransaction(payoutInformation, this.blockTemplate.coinbasevalue);
|
|
||||||
this.block.transactions.unshift(coinbaseTransaction);
|
|
||||||
|
|
||||||
this.block.witnessCommit = bitcoinjs.Block.calculateMerkleRoot(this.block.transactions, true);
|
|
||||||
|
|
||||||
// https://github.com/bitcoin/bips/blob/master/bip-0034.mediawiki
|
|
||||||
const littleEndianBlockHeight = this.convertToLittleEndian(this.blockTemplate.height.toString(16).padStart(6, '0'))
|
|
||||||
|
|
||||||
//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:
|
//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)
|
// 1-byte - OP_RETURN (0x6a)
|
||||||
@@ -56,15 +46,16 @@ export class MiningJob {
|
|||||||
|
|
||||||
// 39th byte onwards: Optional data with no consensus meaning
|
// 39th byte onwards: Optional data with no consensus meaning
|
||||||
const extra = Buffer.from('\\public-pool\\');
|
const extra = Buffer.from('\\public-pool\\');
|
||||||
coinbaseTransaction.ins[0].script = Buffer.concat([Buffer.from([littleEndianBlockHeight.byteLength]), littleEndianBlockHeight, extra, Buffer.alloc(8, 0)]);
|
// https://github.com/bitcoin/bips/blob/master/bip-0034.mediawiki
|
||||||
|
this.coinbaseTransaction.ins[0].script = Buffer.concat([Buffer.from([jobTemplate.blockData.littleEndianBlockHeight.byteLength]), jobTemplate.blockData.littleEndianBlockHeight, extra, Buffer.alloc(8, 0)]);
|
||||||
|
|
||||||
coinbaseTransaction.addOutput(bitcoinjs.script.compile([bitcoinjs.opcodes.OP_RETURN, Buffer.concat([segwitMagicBits, this.block.witnessCommit])]), 0);
|
this.coinbaseTransaction.addOutput(bitcoinjs.script.compile([bitcoinjs.opcodes.OP_RETURN, Buffer.concat([segwitMagicBits, jobTemplate.block.witnessCommit])]), 0);
|
||||||
|
|
||||||
// get the non-witness coinbase tx
|
// get the non-witness coinbase tx
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const serializedCoinbaseTx = coinbaseTransaction.__toBuffer().toString('hex');
|
const serializedCoinbaseTx = this.coinbaseTransaction.__toBuffer().toString('hex');
|
||||||
|
|
||||||
const inputScript = coinbaseTransaction.ins[0].script.toString('hex');
|
const inputScript = this.coinbaseTransaction.ins[0].script.toString('hex');
|
||||||
|
|
||||||
const partOneIndex = serializedCoinbaseTx.indexOf(inputScript) + inputScript.length;
|
const partOneIndex = serializedCoinbaseTx.indexOf(inputScript) + inputScript.length;
|
||||||
|
|
||||||
@@ -72,23 +63,13 @@ export class MiningJob {
|
|||||||
this.coinbasePart2 = serializedCoinbaseTx.slice(partOneIndex);
|
this.coinbasePart2 = serializedCoinbaseTx.slice(partOneIndex);
|
||||||
|
|
||||||
|
|
||||||
// Calculate merkle branch
|
|
||||||
const transactionBuffers = this.block.transactions.map(tx => tx.getHash(false));
|
|
||||||
|
|
||||||
const merkleTree = merkle(transactionBuffers, bitcoinjs.crypto.hash256);
|
|
||||||
const merkleBranches: Buffer[] = merkleProof(merkleTree, transactionBuffers[0]).filter(h => h != null);
|
|
||||||
this.block.merkleRoot = merkleBranches.pop();
|
|
||||||
|
|
||||||
// remove the first (coinbase) and last (root) element from the branch
|
|
||||||
this.merkle_branch = merkleBranches.slice(1, merkleBranches.length).map(b => b.toString('hex'))
|
|
||||||
|
|
||||||
this.block.transactions[0] = coinbaseTransaction;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public copyAndUpdateBlock(versionMask: number, nonce: number, extraNonce: string, extraNonce2: string, timestamp: number): bitcoinjs.Block {
|
public copyAndUpdateBlock(jobTemplate: IJobTemplate, versionMask: number, nonce: number, extraNonce: string, extraNonce2: string, timestamp: number): bitcoinjs.Block {
|
||||||
|
|
||||||
const testBlock = bitcoinjs.Block.fromBuffer(this.block.toBuffer());
|
const testBlock = bitcoinjs.Block.fromBuffer(jobTemplate.block.toBuffer());
|
||||||
|
|
||||||
|
testBlock.transactions[0] = this.coinbaseTransaction;
|
||||||
|
|
||||||
testBlock.nonce = nonce;
|
testBlock.nonce = nonce;
|
||||||
|
|
||||||
@@ -103,7 +84,7 @@ export class MiningJob {
|
|||||||
testBlock.transactions[0].ins[0].script = Buffer.from(`${nonceScript.substring(0, nonceScript.length - 16)}${extraNonce}${extraNonce2}`, 'hex');
|
testBlock.transactions[0].ins[0].script = Buffer.from(`${nonceScript.substring(0, nonceScript.length - 16)}${extraNonce}${extraNonce2}`, 'hex');
|
||||||
|
|
||||||
//recompute the root since we updated the coinbase script with the nonces
|
//recompute the root since we updated the coinbase script with the nonces
|
||||||
testBlock.merkleRoot = this.calculateMerkleRootHash(testBlock.transactions[0].getHash(false), this.merkle_branch);
|
testBlock.merkleRoot = this.calculateMerkleRootHash(testBlock.transactions[0].getHash(false), jobTemplate.merkle_branch);
|
||||||
|
|
||||||
|
|
||||||
testBlock.timestamp = timestamp;
|
testBlock.timestamp = timestamp;
|
||||||
@@ -111,6 +92,7 @@ export class MiningJob {
|
|||||||
return testBlock;
|
return testBlock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private calculateMerkleRootHash(newRoot: Buffer, merkleBranches: string[]): Buffer {
|
private calculateMerkleRootHash(newRoot: Buffer, merkleBranches: string[]): Buffer {
|
||||||
|
|
||||||
const bothMerkles = Buffer.alloc(64);
|
const bothMerkles = Buffer.alloc(64);
|
||||||
@@ -181,21 +163,21 @@ export class MiningJob {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public response(): string {
|
public response(jobTemplate: IJobTemplate): string {
|
||||||
|
|
||||||
const job = {
|
const job = {
|
||||||
id: null,
|
id: null,
|
||||||
method: eResponseMethod.MINING_NOTIFY,
|
method: eResponseMethod.MINING_NOTIFY,
|
||||||
params: [
|
params: [
|
||||||
this.jobId,
|
this.jobId,
|
||||||
this.swapEndianWords(this.block.prevHash).toString('hex'),
|
this.swapEndianWords(jobTemplate.block.prevHash).toString('hex'),
|
||||||
this.coinbasePart1,
|
this.coinbasePart1,
|
||||||
this.coinbasePart2,
|
this.coinbasePart2,
|
||||||
this.merkle_branch,
|
jobTemplate.merkle_branch,
|
||||||
this.block.version.toString(16),
|
jobTemplate.block.version.toString(16),
|
||||||
this.block.bits.toString(16),
|
jobTemplate.block.bits.toString(16),
|
||||||
this.block.timestamp.toString(16),
|
jobTemplate.block.timestamp.toString(16),
|
||||||
this.clean_jobs
|
jobTemplate.blockData.clearJobs
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -203,12 +185,6 @@ export class MiningJob {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private convertToLittleEndian(hash: string): Buffer {
|
|
||||||
const bytes = Buffer.from(hash, 'hex');
|
|
||||||
Array.prototype.reverse.call(bytes);
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
private swapEndianWords(buffer: Buffer): Buffer {
|
private swapEndianWords(buffer: Buffer): Buffer {
|
||||||
const swappedBuffer = Buffer.alloc(buffer.length);
|
const swappedBuffer = Buffer.alloc(buffer.length);
|
||||||
|
|
||||||
@@ -223,15 +199,4 @@ export class MiningJob {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,6 @@ import { ClientEntity } from '../ORM/client/client.entity';
|
|||||||
import { ClientModule } from '../ORM/client/client.module';
|
import { ClientModule } from '../ORM/client/client.module';
|
||||||
import { ClientService } from '../ORM/client/client.service';
|
import { ClientService } from '../ORM/client/client.service';
|
||||||
import { BitcoinRpcService as MockBitcoinRpcService } from '../services/bitcoin-rpc.service';
|
import { BitcoinRpcService as MockBitcoinRpcService } from '../services/bitcoin-rpc.service';
|
||||||
import { BlockTemplateService } from '../services/block-template.service';
|
|
||||||
import { NotificationService } from '../services/notification.service';
|
import { NotificationService } from '../services/notification.service';
|
||||||
import { StratumV1JobsService } from '../services/stratum-v1-jobs.service';
|
import { StratumV1JobsService } from '../services/stratum-v1-jobs.service';
|
||||||
import { IMiningInfo } from './bitcoin-rpc/IMiningInfo';
|
import { IMiningInfo } from './bitcoin-rpc/IMiningInfo';
|
||||||
@@ -37,9 +36,9 @@ describe('StratumV1Client', () => {
|
|||||||
|
|
||||||
|
|
||||||
let promiseSocket: PromiseSocket<any>;
|
let promiseSocket: PromiseSocket<any>;
|
||||||
let stratumV1JobsService: StratumV1JobsService = new StratumV1JobsService();
|
let stratumV1JobsService: StratumV1JobsService;
|
||||||
let bitcoinRpcService: MockBitcoinRpcService;
|
let bitcoinRpcService: MockBitcoinRpcService;
|
||||||
let blockTemplateService: BlockTemplateService;
|
|
||||||
let clientService: ClientService;
|
let clientService: ClientService;
|
||||||
let clientStatisticsService: ClientStatisticsService;
|
let clientStatisticsService: ClientStatisticsService;
|
||||||
let notificationService: NotificationService;
|
let notificationService: NotificationService;
|
||||||
@@ -92,7 +91,7 @@ describe('StratumV1Client', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|
||||||
|
console.log('NEW TEST')
|
||||||
|
|
||||||
clientService = moduleRef.get<ClientService>(ClientService);
|
clientService = moduleRef.get<ClientService>(ClientService);
|
||||||
|
|
||||||
@@ -107,12 +106,11 @@ describe('StratumV1Client', () => {
|
|||||||
configService = moduleRef.get<ConfigService>(ConfigService);
|
configService = moduleRef.get<ConfigService>(ConfigService);
|
||||||
|
|
||||||
bitcoinRpcService = new MockBitcoinRpcService(null);
|
bitcoinRpcService = new MockBitcoinRpcService(null);
|
||||||
|
|
||||||
jest.spyOn(bitcoinRpcService, 'getBlockTemplate').mockReturnValue(Promise.resolve(MockRecording1.BLOCK_TEMPLATE));
|
jest.spyOn(bitcoinRpcService, 'getBlockTemplate').mockReturnValue(Promise.resolve(MockRecording1.BLOCK_TEMPLATE));
|
||||||
bitcoinRpcService.newBlock$ = newBlockEmitter.asObservable();
|
bitcoinRpcService.newBlock$ = newBlockEmitter.asObservable();
|
||||||
|
|
||||||
blockTemplateService = new BlockTemplateService(bitcoinRpcService);
|
|
||||||
|
|
||||||
|
stratumV1JobsService = new StratumV1JobsService(bitcoinRpcService);
|
||||||
|
|
||||||
promiseSocket = new PromiseSocket();
|
promiseSocket = new PromiseSocket();
|
||||||
jest.spyOn(promiseSocket.socket, 'on').mockImplementation((event: string, fn: (data: Buffer) => void) => {
|
jest.spyOn(promiseSocket.socket, 'on').mockImplementation((event: string, fn: (data: Buffer) => void) => {
|
||||||
@@ -125,7 +123,6 @@ describe('StratumV1Client', () => {
|
|||||||
client = new StratumV1Client(
|
client = new StratumV1Client(
|
||||||
promiseSocket,
|
promiseSocket,
|
||||||
stratumV1JobsService,
|
stratumV1JobsService,
|
||||||
blockTemplateService,
|
|
||||||
bitcoinRpcService,
|
bitcoinRpcService,
|
||||||
clientService,
|
clientService,
|
||||||
clientStatisticsService,
|
clientStatisticsService,
|
||||||
@@ -245,7 +242,7 @@ describe('StratumV1Client', () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
expect(promiseSocket.write).lastCalledWith(`{"id":null,"method":"mining.notify","params":["3","171592f223740e92d223f6e68bff25279af7ac4f2246451e0000000200000000","02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1903c943255c7075626c69632d706f6f6c5c","ffffffff037a90000000000000160014e6f22ca44dc800e9d049621a3b9a42c509f1c4bc3b0f250000000000160014e6f22ca44dc800e9d049621a3b9a42c509f1c4bc0000000000000000266a24aa21a9edbd3d1d916aa0b57326a2d88ebe1b68a1d7c48585f26d8335fe6a94b62755f64c00000000",["175335649d5e8746982969ec88f52e85ac9917106fba5468e699c8879ab974a1","d5644ab3e708c54cd68dc5aedc92b8d3037449687f92ec41ed6e37673d969d4a","5c9ec187517edc0698556cca5ce27e54c96acb014770599ed9df4d4937fbf2b0"],"20000000","192495f8","${MockRecording1.TIME}",false]}\n`);
|
expect(promiseSocket.write).lastCalledWith(`{"id":null,"method":"mining.notify","params":["1","171592f223740e92d223f6e68bff25279af7ac4f2246451e0000000200000000","02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff1903c943255c7075626c69632d706f6f6c5c","ffffffff037a90000000000000160014e6f22ca44dc800e9d049621a3b9a42c509f1c4bc3b0f250000000000160014e6f22ca44dc800e9d049621a3b9a42c509f1c4bc0000000000000000266a24aa21a9edbd3d1d916aa0b57326a2d88ebe1b68a1d7c48585f26d8335fe6a94b62755f64c00000000",["175335649d5e8746982969ec88f52e85ac9917106fba5468e699c8879ab974a1","d5644ab3e708c54cd68dc5aedc92b8d3037449687f92ec41ed6e37673d969d4a","5c9ec187517edc0698556cca5ce27e54c96acb014770599ed9df4d4937fbf2b0"],"20000000","192495f8","${MockRecording1.TIME}",false]}\n`);
|
||||||
|
|
||||||
|
|
||||||
socketEmitter(Buffer.from(MockRecording1.MINING_SUBMIT));
|
socketEmitter(Buffer.from(MockRecording1.MINING_SUBMIT));
|
||||||
@@ -253,8 +250,11 @@ describe('StratumV1Client', () => {
|
|||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
await new Promise((r) => setTimeout(r, 100));
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
|
||||||
|
expect(promiseSocket.write).lastCalledWith(`{\"id\":5,\"error\":null,\"result\":true}\n`);
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -6,18 +6,16 @@ import { validate, ValidatorOptions } from 'class-validator';
|
|||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
import { Socket } from 'net';
|
import { Socket } from 'net';
|
||||||
import PromiseSocket from 'promise-socket';
|
import PromiseSocket from 'promise-socket';
|
||||||
import { combineLatest, firstValueFrom, interval, startWith, takeUntil } from 'rxjs';
|
import { firstValueFrom, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
import { BlocksService } from '../ORM/blocks/blocks.service';
|
import { BlocksService } from '../ORM/blocks/blocks.service';
|
||||||
import { ClientStatisticsService } from '../ORM/client-statistics/client-statistics.service';
|
import { ClientStatisticsService } from '../ORM/client-statistics/client-statistics.service';
|
||||||
import { ClientEntity } from '../ORM/client/client.entity';
|
import { ClientEntity } from '../ORM/client/client.entity';
|
||||||
import { ClientService } from '../ORM/client/client.service';
|
import { ClientService } from '../ORM/client/client.service';
|
||||||
import { BitcoinRpcService } from '../services/bitcoin-rpc.service';
|
import { BitcoinRpcService } from '../services/bitcoin-rpc.service';
|
||||||
import { BlockTemplateService } from '../services/block-template.service';
|
|
||||||
import { NotificationService } from '../services/notification.service';
|
import { NotificationService } from '../services/notification.service';
|
||||||
import { StratumV1JobsService } from '../services/stratum-v1-jobs.service';
|
import { IJobTemplate, StratumV1JobsService } from '../services/stratum-v1-jobs.service';
|
||||||
import { EasyUnsubscribe } from '../utils/EasyUnsubscribe';
|
import { EasyUnsubscribe } from '../utils/EasyUnsubscribe';
|
||||||
import { IBlockTemplate } from './bitcoin-rpc/IBlockTemplate';
|
|
||||||
import { eRequestMethod } from './enums/eRequestMethod';
|
import { eRequestMethod } from './enums/eRequestMethod';
|
||||||
import { eResponseMethod } from './enums/eResponseMethod';
|
import { eResponseMethod } from './enums/eResponseMethod';
|
||||||
import { eStratumErrorCode } from './enums/eStratumErrorCode';
|
import { eStratumErrorCode } from './enums/eStratumErrorCode';
|
||||||
@@ -49,7 +47,6 @@ export class StratumV1Client extends EasyUnsubscribe {
|
|||||||
constructor(
|
constructor(
|
||||||
public readonly promiseSocket: PromiseSocket<Socket>,
|
public readonly promiseSocket: PromiseSocket<Socket>,
|
||||||
private readonly stratumV1JobsService: StratumV1JobsService,
|
private readonly stratumV1JobsService: StratumV1JobsService,
|
||||||
private readonly blockTemplateService: BlockTemplateService,
|
|
||||||
private readonly bitcoinRpcService: BitcoinRpcService,
|
private readonly bitcoinRpcService: BitcoinRpcService,
|
||||||
private readonly clientService: ClientService,
|
private readonly clientService: ClientService,
|
||||||
private readonly clientStatisticsService: ClientStatisticsService,
|
private readonly clientStatisticsService: ClientStatisticsService,
|
||||||
@@ -274,47 +271,24 @@ export class StratumV1Client extends EasyUnsubscribe {
|
|||||||
startTime: new Date(),
|
startTime: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let lastIntervalCount = undefined;
|
this.stratumV1JobsService.newMiningJob$.pipe(
|
||||||
let skipNext = false;
|
|
||||||
combineLatest([this.blockTemplateService.currentBlockTemplate$, interval(60000).pipe(startWith(-1))])
|
|
||||||
.pipe(
|
|
||||||
takeUntil(this.easyUnsubscribe)
|
takeUntil(this.easyUnsubscribe)
|
||||||
)
|
).subscribe(async (jobTemplate) => {
|
||||||
.subscribe(async ([{ blockTemplate }, interValCount]) => {
|
await this.sendNewMiningJob(jobTemplate);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let clearJobs = false;
|
|
||||||
if (lastIntervalCount === interValCount) {
|
|
||||||
clearJobs = true;
|
|
||||||
skipNext = true;
|
|
||||||
console.log('new block')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (skipNext == true && clearJobs == false) {
|
|
||||||
skipNext = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastIntervalCount = interValCount;
|
|
||||||
|
|
||||||
await this.sendNewMiningJob(blockTemplate, clearJobs);
|
|
||||||
|
|
||||||
await this.checkDifficulty();
|
await this.checkDifficulty();
|
||||||
|
})
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sendNewMiningJob(blockTemplate: IBlockTemplate, clearJobs: boolean) {
|
private async sendNewMiningJob(jobTemplate: IJobTemplate) {
|
||||||
|
|
||||||
const hashRate = await this.clientStatisticsService.getHashRateForSession(this.clientAuthorization.address, this.clientAuthorization.worker, this.extraNonceAndSessionId);
|
const hashRate = await this.clientStatisticsService.getHashRateForSession(this.clientAuthorization.address, this.clientAuthorization.worker, this.extraNonceAndSessionId);
|
||||||
|
|
||||||
let payoutInformation;
|
let payoutInformation;
|
||||||
//10Th/s
|
//50Th/s
|
||||||
const noFee = hashRate < 10000000000000;
|
const noFee = hashRate < 50000000000000;
|
||||||
if (noFee) {
|
if (noFee) {
|
||||||
payoutInformation = [
|
payoutInformation = [
|
||||||
{ address: this.clientAuthorization.address, percent: 100 }
|
{ address: this.clientAuthorization.address, percent: 100 }
|
||||||
@@ -332,21 +306,20 @@ export class StratumV1Client extends EasyUnsubscribe {
|
|||||||
this.configService.get('NETWORK') === 'mainnet' ? bitcoinjs.networks.bitcoin : bitcoinjs.networks.testnet,
|
this.configService.get('NETWORK') === 'mainnet' ? bitcoinjs.networks.bitcoin : bitcoinjs.networks.testnet,
|
||||||
this.stratumV1JobsService.getNextId(),
|
this.stratumV1JobsService.getNextId(),
|
||||||
payoutInformation,
|
payoutInformation,
|
||||||
blockTemplate,
|
jobTemplate
|
||||||
clearJobs
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.stratumV1JobsService.addJob(job, clearJobs);
|
this.stratumV1JobsService.addJob(job, jobTemplate.blockData.clearJobs);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.promiseSocket.write(job.response());
|
await this.promiseSocket.write(job.response(jobTemplate));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await this.promiseSocket.end();
|
await this.promiseSocket.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Sent new job to ${this.clientAuthorization.worker}.${this.extraNonceAndSessionId}. (clearJobs: ${clearJobs}, fee?: ${!noFee})`)
|
console.log(`Sent new job to ${this.clientAuthorization.worker}.${this.extraNonceAndSessionId}. (clearJobs: ${jobTemplate.blockData.clearJobs}, fee?: ${!noFee})`)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,6 +327,7 @@ export class StratumV1Client extends EasyUnsubscribe {
|
|||||||
private async handleMiningSubmission(submission: MiningSubmitMessage) {
|
private async handleMiningSubmission(submission: MiningSubmitMessage) {
|
||||||
|
|
||||||
const job = this.stratumV1JobsService.getJobById(submission.jobId);
|
const job = this.stratumV1JobsService.getJobById(submission.jobId);
|
||||||
|
const jobTemplate = this.stratumV1JobsService.getJobTemplateById(job.jobTemplateId);
|
||||||
// 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) {
|
if (job == null) {
|
||||||
const err = new StratumErrorMessage(
|
const err = new StratumErrorMessage(
|
||||||
@@ -365,6 +339,7 @@ export class StratumV1Client extends EasyUnsubscribe {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const updatedJobBlock = job.copyAndUpdateBlock(
|
const updatedJobBlock = job.copyAndUpdateBlock(
|
||||||
|
jobTemplate,
|
||||||
parseInt(submission.versionMask, 16),
|
parseInt(submission.versionMask, 16),
|
||||||
parseInt(submission.nonce, 16),
|
parseInt(submission.nonce, 16),
|
||||||
this.extraNonceAndSessionId,
|
this.extraNonceAndSessionId,
|
||||||
@@ -375,7 +350,7 @@ export class StratumV1Client extends EasyUnsubscribe {
|
|||||||
const { submissionDifficulty, submissionHash } = this.calculateDifficulty(header);
|
const { submissionDifficulty, submissionHash } = this.calculateDifficulty(header);
|
||||||
|
|
||||||
console.log(`DIFF: ${submissionDifficulty} of ${this.sessionDifficulty} from ${this.clientAuthorization.worker + '.' + this.extraNonceAndSessionId}`);
|
console.log(`DIFF: ${submissionDifficulty} of ${this.sessionDifficulty} from ${this.clientAuthorization.worker + '.' + this.extraNonceAndSessionId}`);
|
||||||
console.log(`Header: ${header.toString('hex')}`);
|
//console.log(`Header: ${header.toString('hex')}`);
|
||||||
|
|
||||||
if (submissionDifficulty >= this.sessionDifficulty) {
|
if (submissionDifficulty >= this.sessionDifficulty) {
|
||||||
|
|
||||||
@@ -384,13 +359,13 @@ export class StratumV1Client extends EasyUnsubscribe {
|
|||||||
const blockHex = updatedJobBlock.toHex(false);
|
const blockHex = updatedJobBlock.toHex(false);
|
||||||
const result = await this.bitcoinRpcService.SUBMIT_BLOCK(blockHex);
|
const result = await this.bitcoinRpcService.SUBMIT_BLOCK(blockHex);
|
||||||
await this.blocksService.save({
|
await this.blocksService.save({
|
||||||
height: job.blockTemplate.height,
|
height: jobTemplate.height,
|
||||||
minerAddress: this.clientAuthorization.address,
|
minerAddress: this.clientAuthorization.address,
|
||||||
worker: this.clientAuthorization.worker,
|
worker: this.clientAuthorization.worker,
|
||||||
sessionId: this.extraNonceAndSessionId,
|
sessionId: this.extraNonceAndSessionId,
|
||||||
blockData: blockHex
|
blockData: blockHex
|
||||||
});
|
});
|
||||||
await this.notificationService.notifySubscribersBlockFound(this.clientAuthorization.address, job.blockTemplate.height, updatedJobBlock, result);
|
await this.notificationService.notifySubscribersBlockFound(this.clientAuthorization.address, jobTemplate.height, updatedJobBlock, result);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await this.statistics.addSubmission(this.entity, submissionHash, this.sessionDifficulty);
|
await this.statistics.addSubmission(this.entity, submissionHash, this.sessionDifficulty);
|
||||||
@@ -444,8 +419,8 @@ export class StratumV1Client extends EasyUnsubscribe {
|
|||||||
await this.promiseSocket.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
|
// 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$);
|
const jobTemplate = await firstValueFrom(this.stratumV1JobsService.newMiningJob$);
|
||||||
await this.sendNewMiningJob(blockTemplate, true);
|
await this.sendNewMiningJob(jobTemplate);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { from, map, Observable, shareReplay, switchMap } from 'rxjs';
|
|
||||||
|
|
||||||
import { IBlockTemplate } from '../models/bitcoin-rpc/IBlockTemplate';
|
|
||||||
import { BitcoinRpcService } from './bitcoin-rpc.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class BlockTemplateService {
|
|
||||||
|
|
||||||
|
|
||||||
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 } }))),
|
|
||||||
shareReplay({ refCount: true, bufferSize: 1 })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,153 @@
|
|||||||
import { MiningJob } from '../models/MiningJob';
|
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||||
|
import * as merkle from 'merkle-lib';
|
||||||
|
import * as merkleProof from 'merkle-lib/proof';
|
||||||
|
import { combineLatest, filter, from, interval, map, Observable, shareReplay, startWith, switchMap, tap } from 'rxjs';
|
||||||
|
|
||||||
|
import { IBlockTemplate } from '../models/bitcoin-rpc/IBlockTemplate';
|
||||||
|
import { MiningJob } from '../models/MiningJob';
|
||||||
|
import { BitcoinRpcService } from './bitcoin-rpc.service';
|
||||||
|
|
||||||
|
export interface IJobTemplate {
|
||||||
|
|
||||||
|
block: bitcoinjs.Block;
|
||||||
|
merkle_branch: string[];
|
||||||
|
blockData: {
|
||||||
|
id: string,
|
||||||
|
littleEndianBlockHeight: Buffer;
|
||||||
|
coinbasevalue: number;
|
||||||
|
networkDifficulty: number;
|
||||||
|
clearJobs: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class StratumV1JobsService {
|
export class StratumV1JobsService {
|
||||||
|
|
||||||
|
private lastIntervalCount: number;
|
||||||
|
private skipNext: boolean = false;
|
||||||
|
public newMiningJob$: Observable<IJobTemplate>;
|
||||||
|
|
||||||
public latestJobId: number = 1;
|
public latestJobId: number = 1;
|
||||||
|
|
||||||
public jobs: MiningJob[] = [];
|
public jobs: MiningJob[] = [];
|
||||||
|
|
||||||
|
public blocks: { [id: number]: IJobTemplate } = {};
|
||||||
|
|
||||||
|
private currentBlockTemplate$: Observable<{ blockTemplate: IBlockTemplate }>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly bitcoinRpcService: BitcoinRpcService
|
||||||
|
) {
|
||||||
|
this.currentBlockTemplate$ = this.bitcoinRpcService.newBlock$.pipe(
|
||||||
|
switchMap((miningInfo) => {
|
||||||
|
return from(this.bitcoinRpcService.getBlockTemplate())
|
||||||
|
.pipe(map(blockTemplate => { return { miningInfo, blockTemplate } }))
|
||||||
|
}
|
||||||
|
),
|
||||||
|
shareReplay({ refCount: true, bufferSize: 1 })
|
||||||
|
);
|
||||||
|
|
||||||
|
this.newMiningJob$ = combineLatest([this.currentBlockTemplate$, interval(60000).pipe(startWith(-1))]).pipe(
|
||||||
|
map(([{ blockTemplate }, interValCount]) => {
|
||||||
|
|
||||||
|
let clearJobs = false;
|
||||||
|
if (this.lastIntervalCount === interValCount) {
|
||||||
|
clearJobs = true;
|
||||||
|
this.skipNext = true;
|
||||||
|
console.log('new block')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.skipNext == true && clearJobs == false) {
|
||||||
|
this.skipNext = false;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastIntervalCount = interValCount;
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: blockTemplate.version,
|
||||||
|
bits: parseInt(blockTemplate.bits, 16),
|
||||||
|
prevHash: this.convertToLittleEndian(blockTemplate.previousblockhash),
|
||||||
|
transactions: blockTemplate.transactions.map(t => bitcoinjs.Transaction.fromHex(t.data)),
|
||||||
|
littleEndianBlockHeight: this.convertToLittleEndian(blockTemplate.height.toString(16).padStart(6, '0')),
|
||||||
|
coinbasevalue: blockTemplate.coinbasevalue,
|
||||||
|
timestamp: Math.floor(new Date().getTime() / 1000),
|
||||||
|
networkDifficulty: this.calculateNetworkDifficulty(parseInt(blockTemplate.bits, 16)),
|
||||||
|
clearJobs
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
filter(next => next != null),
|
||||||
|
map(({ version, bits, prevHash, transactions, timestamp, littleEndianBlockHeight, coinbasevalue, networkDifficulty, clearJobs }) => {
|
||||||
|
const block = new bitcoinjs.Block();
|
||||||
|
|
||||||
|
//create an empty coinbase tx
|
||||||
|
const tempCoinbaseTx = new bitcoinjs.Transaction();
|
||||||
|
tempCoinbaseTx.version = 2;
|
||||||
|
tempCoinbaseTx.addInput(Buffer.alloc(32, 0), 0xffffffff, 0xffffffff);
|
||||||
|
tempCoinbaseTx.ins[0].witness = [Buffer.alloc(32, 0)];
|
||||||
|
transactions.unshift(tempCoinbaseTx);
|
||||||
|
|
||||||
|
|
||||||
|
const transactionBuffers = transactions.map(tx => tx.getHash(false));
|
||||||
|
|
||||||
|
const merkleTree = merkle(transactionBuffers, bitcoinjs.crypto.hash256);
|
||||||
|
const merkleBranches: Buffer[] = merkleProof(merkleTree, transactionBuffers[0]).filter(h => h != null);
|
||||||
|
block.merkleRoot = merkleBranches.pop();
|
||||||
|
|
||||||
|
// remove the first (coinbase) and last (root) element from the branch
|
||||||
|
const merkle_branch = merkleBranches.slice(1, merkleBranches.length).map(b => b.toString('hex'))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
block.prevHash = prevHash;
|
||||||
|
block.version = version;
|
||||||
|
block.bits = bits;
|
||||||
|
block.timestamp = timestamp;
|
||||||
|
|
||||||
|
block.transactions = transactions;
|
||||||
|
block.witnessCommit = bitcoinjs.Block.calculateMerkleRoot(transactions, true);
|
||||||
|
|
||||||
|
return {
|
||||||
|
block,
|
||||||
|
merkle_branch,
|
||||||
|
blockData: {
|
||||||
|
id: this.getNextId(),
|
||||||
|
littleEndianBlockHeight,
|
||||||
|
coinbasevalue,
|
||||||
|
networkDifficulty,
|
||||||
|
clearJobs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
tap((data) => {
|
||||||
|
if (data.blockData.clearJobs) {
|
||||||
|
this.blocks = {};
|
||||||
|
}
|
||||||
|
this.blocks[data.blockData.id] = data;
|
||||||
|
}),
|
||||||
|
shareReplay({ refCount: true, bufferSize: 1 })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertToLittleEndian(hash: string): Buffer {
|
||||||
|
const bytes = Buffer.from(hash, 'hex');
|
||||||
|
Array.prototype.reverse.call(bytes);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getJobTemplateById(jobTemplateId: string) {
|
||||||
|
return this.blocks[jobTemplateId];
|
||||||
|
}
|
||||||
|
|
||||||
public addJob(job: MiningJob, clearJobs: boolean) {
|
public addJob(job: MiningJob, clearJobs: boolean) {
|
||||||
if (clearJobs) {
|
if (clearJobs) {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { BlocksService } from '../ORM/blocks/blocks.service';
|
|||||||
import { ClientStatisticsService } from '../ORM/client-statistics/client-statistics.service';
|
import { ClientStatisticsService } from '../ORM/client-statistics/client-statistics.service';
|
||||||
import { ClientService } from '../ORM/client/client.service';
|
import { ClientService } from '../ORM/client/client.service';
|
||||||
import { BitcoinRpcService } from './bitcoin-rpc.service';
|
import { BitcoinRpcService } from './bitcoin-rpc.service';
|
||||||
import { BlockTemplateService } from './block-template.service';
|
|
||||||
import { NotificationService } from './notification.service';
|
import { NotificationService } from './notification.service';
|
||||||
import { StratumV1JobsService } from './stratum-v1-jobs.service';
|
import { StratumV1JobsService } from './stratum-v1-jobs.service';
|
||||||
|
|
||||||
@@ -18,7 +17,6 @@ export class StratumV1Service implements OnModuleInit {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly bitcoinRpcService: BitcoinRpcService,
|
private readonly bitcoinRpcService: BitcoinRpcService,
|
||||||
private readonly blockTemplateService: BlockTemplateService,
|
|
||||||
private readonly clientService: ClientService,
|
private readonly clientService: ClientService,
|
||||||
private readonly clientStatisticsService: ClientStatisticsService,
|
private readonly clientStatisticsService: ClientStatisticsService,
|
||||||
private readonly notificationService: NotificationService,
|
private readonly notificationService: NotificationService,
|
||||||
@@ -43,8 +41,7 @@ export class StratumV1Service implements OnModuleInit {
|
|||||||
|
|
||||||
const client = new StratumV1Client(
|
const client = new StratumV1Client(
|
||||||
promiseSocket,
|
promiseSocket,
|
||||||
new StratumV1JobsService(),
|
new StratumV1JobsService(this.bitcoinRpcService),
|
||||||
this.blockTemplateService,
|
|
||||||
this.bitcoinRpcService,
|
this.bitcoinRpcService,
|
||||||
this.clientService,
|
this.clientService,
|
||||||
this.clientStatisticsService,
|
this.clientStatisticsService,
|
||||||
|
|||||||
Reference in New Issue
Block a user