This commit is contained in:
Ben Wilson 2023-06-24 20:20:41 -04:00
parent f61e9f1417
commit 9315d200ff
11 changed files with 199 additions and 62 deletions

View File

@ -1,5 +1,6 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { DateTimeTransformer } from '../utils/DateTimeTransformer';
import { TrackedEntity } from '../utils/TrackedEntity.entity';
@Entity()
@ -17,7 +18,10 @@ export class ClientStatisticsEntity extends TrackedEntity {
@Column({ length: 8, type: 'varchar' })
sessionId: string;
@Column({ type: 'datetime' })
@Column({
type: 'datetime',
transformer: new DateTimeTransformer()
})
time: Date;
@Column()

View File

@ -19,7 +19,7 @@ export class ClientStatisticsService {
return await this.clientStatisticsRepository.save(clientStatistic);
}
public async getHashRate(sessionId: string) {
public async getHashRateForAddress(address: string) {
const query = `
SELECT
@ -28,10 +28,10 @@ export class ClientStatisticsService {
FROM
client_statistics_entity AS entry
WHERE
entry.sessionId = ? AND entry.time > datetime("now", "-1 hour")
entry.address = ? AND entry.time > datetime("now", "-1 hour")
`;
const result = await this.clientStatisticsRepository.query(query, [sessionId]);
const result = await this.clientStatisticsRepository.query(query, [address]);
const timeDiff = result[0].timeDiff;
const difficultySum = result[0].difficultySum;
@ -40,26 +40,125 @@ export class ClientStatisticsService {
}
public async getChartData(sessionId: string) {
public async getChartDataForAddress(address: string) {
const query = `
WITH result_set AS (
SELECT
MAX(time) AS label,
MAX(time) || 'GMT' AS label,
(SUM(difficulty) * 4294967296) /
((JULIANDAY(MAX(time)) - JULIANDAY(MIN(time))) * 24 * 60 * 60 * 1000000000) AS data
FROM
client_statistics_entity AS entry
WHERE
entry.sessionId = ? AND entry.time > datetime("now", "-1 day")
entry.address = ? AND entry.time > datetime("now", "-1 day")
GROUP BY
strftime('%Y-%m-%d %H', time, 'localtime') || (strftime('%M', time, 'localtime') / 5)
ORDER BY
time
)
SELECT *
FROM result_set
WHERE label <> (SELECT MAX(label) FROM result_set);
`;
const result = await this.clientStatisticsRepository.query(query, [address]);
return result;
}
public async getHashRateForGroup(address: string, clientName: string) {
const query = `
SELECT
(JULIANDAY(MAX(entry.time)) - JULIANDAY(MIN(entry.time))) * 24 * 60 * 60 AS timeDiff,
SUM(entry.difficulty) AS difficultySum
FROM
client_statistics_entity AS entry
WHERE
entry.address = ? AND entry.clientName = ? AND entry.time > datetime("now", "-1 hour")
`;
const result = await this.clientStatisticsRepository.query(query, [address, clientName]);
const timeDiff = result[0].timeDiff;
const difficultySum = result[0].difficultySum;
return (difficultySum * 4294967296) / (timeDiff * 1000000000);
}
public async getChartDataForGroup(address: string, clientName: string) {
const query = `
WITH result_set AS (
SELECT
MAX(time) || 'GMT' AS label,
(SUM(difficulty) * 4294967296) /
((JULIANDAY(MAX(time)) - JULIANDAY(MIN(time))) * 24 * 60 * 60 * 1000000000) AS data
FROM
client_statistics_entity AS entry
WHERE
entry.address = ? AND entry.clientName = ? AND entry.time > datetime("now", "-1 day")
GROUP BY
strftime('%Y-%m-%d %H', time, 'localtime') || (strftime('%M', time, 'localtime') / 5)
ORDER BY
time
)
SELECT *
FROM result_set
WHERE label <> (SELECT MAX(Label) FROM result_set);
WHERE label <> (SELECT MAX(label) FROM result_set);
`;
const result = await this.clientStatisticsRepository.query(query, [sessionId]);
const result = await this.clientStatisticsRepository.query(query, [address, clientName]);
return result;
}
public async getHashRateForSession(address: string, clientName: string, sessionId: string) {
const query = `
SELECT
(JULIANDAY(MAX(entry.time)) - JULIANDAY(MIN(entry.time))) * 24 * 60 * 60 AS timeDiff,
SUM(entry.difficulty) AS difficultySum
FROM
client_statistics_entity AS entry
WHERE
entry.address = ? AND entry.clientName = ? AND entry.sessionId = ? AND entry.time > datetime("now", "-1 hour")
`;
const result = await this.clientStatisticsRepository.query(query, [address, clientName, sessionId]);
const timeDiff = result[0].timeDiff;
const difficultySum = result[0].difficultySum;
return (difficultySum * 4294967296) / (timeDiff * 1000000000);
}
public async getChartDataForSession(address: string, clientName: string, sessionId: string) {
const query = `
WITH result_set AS (
SELECT
MAX(time) || 'GMT' AS label,
(SUM(difficulty) * 4294967296) /
((JULIANDAY(MAX(time)) - JULIANDAY(MIN(time))) * 24 * 60 * 60 * 1000000000) AS data
FROM
client_statistics_entity AS entry
WHERE
entry.address = ? AND entry.clientName = ? AND entry.sessionId = ? AND entry.time > datetime("now", "-1 day")
GROUP BY
strftime('%Y-%m-%d %H', time, 'localtime') || (strftime('%M', time, 'localtime') / 5)
ORDER BY
time
)
SELECT *
FROM result_set
WHERE label <> (SELECT MAX(label) FROM result_set);
`;
const result = await this.clientStatisticsRepository.query(query, [address, clientName, sessionId]);
return result;
}

View File

@ -1,5 +1,6 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { DateTimeTransformer } from '../utils/DateTimeTransformer';
import { TrackedEntity } from '../utils/TrackedEntity.entity';
@Entity()
@ -18,7 +19,7 @@ export class ClientEntity extends TrackedEntity {
@Column({ length: 8, type: 'varchar' })
sessionId: string;
@Column({ type: 'datetime' })
@Column({ type: 'datetime', transformer: new DateTimeTransformer() })
startTime: Date;

View File

@ -40,7 +40,17 @@ export class ClientService {
})
}
public async getById(address: string, clientName: string, sessionId: string): Promise<ClientEntity> {
public async getByName(address: string, clientName: string): Promise<ClientEntity[]> {
return await this.clientRepository.find({
where: {
address,
clientName
}
})
}
public async getBySessionId(address: string, clientName: string, sessionId: string): Promise<ClientEntity> {
return await this.clientRepository.findOne({
where: {
address,

View File

@ -0,0 +1,14 @@
import { ValueTransformer } from 'typeorm';
export class DateTimeTransformer implements ValueTransformer {
to(value: Date): any {
// Convert the local time to UTC before saving to the database
const utcTime = value?.toLocaleString();
return utcTime;
}
from(value: any): Date {
// Convert the UTC time from the database to the local time zone
return value;
}
}

View File

@ -1,12 +1,14 @@
import { CreateDateColumn, DeleteDateColumn, UpdateDateColumn } from 'typeorm';
import { DateTimeTransformer } from './DateTimeTransformer';
export abstract class TrackedEntity {
@DeleteDateColumn({ nullable: true, type: 'datetime' })
@DeleteDateColumn({ nullable: true, type: 'datetime', transformer: new DateTimeTransformer() })
public deletedAt?: Date;
@CreateDateColumn({ type: 'datetime' })
@CreateDateColumn({ type: 'datetime', transformer: new DateTimeTransformer() })
public createdAt?: Date
@UpdateDateColumn({ type: 'datetime' })
@UpdateDateColumn({ type: 'datetime', transformer: new DateTimeTransformer() })
public updatedAt?: Date
}

View File

@ -26,6 +26,7 @@ export class AppController {
const workers = await this.clientService.getByAddress(address);
const chartData = await this.clientStatisticsService.getChartDataForAddress(address);
return {
workersCount: workers.length,
@ -35,27 +36,45 @@ export class AppController {
sessionId: worker.sessionId,
name: worker.clientName,
bestDifficulty: Math.floor(worker.bestDifficulty),
hashRate: Math.floor(await this.clientStatisticsService.getHashRate(worker.sessionId)),
hashRate: Math.floor(await this.clientStatisticsService.getHashRateForSession(worker.address, worker.clientName, worker.sessionId)),
startTime: worker.startTime
};
})
)
),
chartData
}
}
@Get('client/:address/:workerName')
async getWorkerGroupInfo(@Param('address') address: string, @Param('address') workerName: string) {
async getWorkerGroupInfo(@Param('address') address: string, @Param('workerName') workerName: string) {
const workers = await this.clientService.getByName(address, workerName);
const bestDifficulty = workers.reduce((pre, cur, idx, arr) => {
if (cur.bestDifficulty > pre) {
return cur.bestDifficulty;
}
return pre;
}, 0);
const chartData = await this.clientStatisticsService.getChartDataForGroup(address, workerName);
return {
name: workerName,
bestDifficulty: Math.floor(bestDifficulty),
chartData: chartData,
}
}
@Get('client/:address/:workerName/:sessionId')
async getWorkerInfo(@Param('address') address: string, @Param('workerName') workerName: string, @Param('sessionId') sessionId: string) {
const worker = await this.clientService.getById(address, workerName, sessionId);
const worker = await this.clientService.getBySessionId(address, workerName, sessionId);
if (worker == null) {
return new NotFoundException();
}
const chartData = await this.clientStatisticsService.getChartData(sessionId);
const chartData = await this.clientStatisticsService.getChartDataForSession(worker.address, worker.clientName, worker.sessionId);
return {
sessionId: worker.sessionId,

View File

@ -10,14 +10,8 @@ interface AddressObject {
percent: number;
}
export class MiningJob {
public id: number;
public method: eResponseMethod.MINING_NOTIFY;
public params: string[];
public target: string;
public merkleRoot: string;
public job_id: 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 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).
@ -25,20 +19,21 @@ export class MiningJob {
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 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;
public versionMask: string;
public tree: MerkleTree;
private tree: MerkleTree;
public coinbaseTransaction: bitcoinjs.Transaction;
public block: bitcoinjs.Block = new bitcoinjs.Block();
constructor(id: string, payoutInformation: AddressObject[], public blockTemplate: IBlockTemplate, public networkDifficulty: number, public clean_jobs: boolean) {
this.job_id = id;
this.target = blockTemplate.target;
this.jobId = id;
//this.target = blockTemplate.target;
this.prevhash = this.convertToLittleEndian(blockTemplate.previousblockhash);
this.version = blockTemplate.version;
@ -81,8 +76,6 @@ export class MiningJob {
this.tree = new MerkleTree(transactionBuffers, this.sha256, { isBitcoinTree: true });
const rootBuffer = this.tree.getRoot();
this.merkleRoot = rootBuffer.toString('hex');
this.merkle_branch = this.tree.getProof(this.coinbaseTransaction.getHash(false)).map(p => p.data.toString('hex'));
@ -92,41 +85,36 @@ export class MiningJob {
private createCoinbaseTransaction(addresses: AddressObject[], blockHeight: number, reward: number): bitcoinjs.Transaction {
// Part 1
const tx = new bitcoinjs.Transaction();
const coinbaseTransaction = new bitcoinjs.Transaction();
// Set the version of the transaction
tx.version = 2;
coinbaseTransaction.version = 2;
const blockHeightScript = `03${blockHeight.toString(16).padStart(8, '0')}` + '00000000' + '00000000';
// const inputScriptBytes = ((blockHeightScript.length + 16) / 2).toString(16).padStart(2, '0');
// const OP_RETURN = '6a';
// const inputScript = `${OP_RETURN}${inputScriptBytes}${blockHeightScript}`
const inputScript = bitcoinjs.script.compile([bitcoinjs.opcodes.OP_RETURN, Buffer.from(blockHeightScript, 'hex')])
// Add the coinbase input (input with no previous output)
tx.addInput(Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'), 0xffffffff, 0xffffffff, inputScript);
coinbaseTransaction.addInput(Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'), 0xffffffff, 0xffffffff, inputScript);
// Add an output
const recipientAddress = addresses[0].address;
const scriptPubKey = bitcoinjs.payments.p2wpkh({ address: recipientAddress, network: bitcoinjs.networks.testnet });
tx.addOutput(scriptPubKey.output, reward);
let rewardBalance = reward;
addresses.forEach(recipientAddress => {
const scriptPubKey = bitcoinjs.payments.p2wpkh({ address: recipientAddress.address, network: bitcoinjs.networks.testnet });
const amount = Math.floor((recipientAddress.percent / 100) * reward);
rewardBalance -= amount;
coinbaseTransaction.addOutput(scriptPubKey.output, amount);
})
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];
tx.ins[0].witness = [segwitWitnessReservedValue];
return tx;
return coinbaseTransaction;
}
private sha256(data) {
@ -140,7 +128,7 @@ export class MiningJob {
id: null,
method: eResponseMethod.MINING_NOTIFY,
params: [
this.job_id,
this.jobId,
this.prevhash,
this.coinb1,
this.coinb2,

View File

@ -34,7 +34,7 @@ export class StratumV1Client extends EasyUnsubscribe {
public statistics: StratumV1ClientStatistics;
public id: string;
public extraNonce: string;
public stratumInitialized = false;
public refreshInterval: NodeJS.Timer;
@ -57,9 +57,9 @@ export class StratumV1Client extends EasyUnsubscribe {
super();
this.startTime = new Date();
this.statistics = new StratumV1ClientStatistics(this.clientStatisticsService);
this.id = this.getRandomHexString();
this.extraNonce = this.getRandomHexString();
console.log(`New client ID: : ${this.id}`);
console.log(`New client ID: : ${this.extraNonce}`);
this.socket.on('data', this.handleData.bind(this, this.socket));
@ -110,7 +110,7 @@ export class StratumV1Client extends EasyUnsubscribe {
if (errors.length === 0) {
this.clientSubscription = subscriptionMessage;
socket.write(JSON.stringify(this.clientSubscription.response(this.id)) + '\n');
socket.write(JSON.stringify(this.clientSubscription.response(this.extraNonce)) + '\n');
} else {
console.error(errors);
}
@ -223,7 +223,7 @@ export class StratumV1Client extends EasyUnsubscribe {
this.entity = await this.clientService.save({
sessionId: this.id,
sessionId: this.extraNonce,
address: this.clientAuthorization.address,
clientName: this.clientAuthorization.worker,
startTime: new Date(),
@ -260,14 +260,14 @@ export class StratumV1Client extends EasyUnsubscribe {
if (job == null) {
return;
}
const diff = submission.calculateDifficulty(this.id, job, submission);
const diff = submission.calculateDifficulty(this.extraNonce, job, submission);
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.id, diff);
await this.clientService.updateBestDifficulty(this.extraNonce, diff);
this.entity.bestDifficulty = diff;
}
if (diff >= (networkDifficulty / 2)) {
@ -294,7 +294,7 @@ export class StratumV1Client extends EasyUnsubscribe {
private constructBlockAndBroadcast(job: MiningJob, submission: MiningSubmitMessage) {
const block = new Block();
const blockHeightScript = `03${job.blockTemplate.height.toString(16).padStart(8, '0')}${job.id}${submission.extraNonce2}`;
const blockHeightScript = `03${job.blockTemplate.height.toString(16).padStart(8, '0')}${this.extraNonce}${submission.extraNonce2}`;
const inputScript = bitcoinjs.script.compile([bitcoinjs.opcodes.OP_RETURN, Buffer.from(blockHeightScript, 'hex')]);
job.coinbaseTransaction.ins[0].script = inputScript;

View File

@ -21,7 +21,7 @@ export class StratumV1JobsService {
}
public getJobById(jobId: string) {
return this.jobs.find(job => job.job_id == jobId);
return this.jobs.find(job => job.jobId == jobId);
}
public getNextId() {

View File

@ -47,7 +47,7 @@ export class StratumV1Service implements OnModuleInit {
socket.on('end', async () => {
// Handle socket disconnection
await this.clientService.delete(client.id);
await this.clientService.delete(client.extraNonce);
const clientCount = await this.clientService.connectedClientCount();
console.log(`Client disconnected: ${socket.remoteAddress}, ${clientCount} total clients`);
@ -55,7 +55,7 @@ export class StratumV1Service implements OnModuleInit {
socket.on('error', async (error: Error) => {
await this.clientService.delete(client.id);
await this.clientService.delete(client.extraNonce);
const clientCount = await this.clientService.connectedClientCount();
console.error(`Socket error:`, error);