mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 17:15:03 -07:00
optimize share submission path
This commit is contained in:
Generated
-31
@@ -19,7 +19,6 @@
|
|||||||
"@nestjs/schedule": "^3.0.1",
|
"@nestjs/schedule": "^3.0.1",
|
||||||
"@nestjs/typeorm": "^10.0.0",
|
"@nestjs/typeorm": "^10.0.0",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
"big.js": "^6.2.1",
|
|
||||||
"bitcoin-address-validation": "^2.2.1",
|
"bitcoin-address-validation": "^2.2.1",
|
||||||
"bitcoinjs-lib": "^6.1.3",
|
"bitcoinjs-lib": "^6.1.3",
|
||||||
"bitcoinjs-message": "^2.2.0",
|
"bitcoinjs-message": "^2.2.0",
|
||||||
@@ -44,7 +43,6 @@
|
|||||||
"@nestjs/cli": "^9.0.0",
|
"@nestjs/cli": "^9.0.0",
|
||||||
"@nestjs/schematics": "^9.0.0",
|
"@nestjs/schematics": "^9.0.0",
|
||||||
"@nestjs/testing": "^9.0.0",
|
"@nestjs/testing": "^9.0.0",
|
||||||
"@types/big.js": "^6.1.6",
|
|
||||||
"@types/cron": "^2.0.1",
|
"@types/cron": "^2.0.1",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/jest": "29.5.1",
|
"@types/jest": "29.5.1",
|
||||||
@@ -2219,12 +2217,6 @@
|
|||||||
"@babel/types": "^7.20.7"
|
"@babel/types": "^7.20.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/big.js": {
|
|
||||||
"version": "6.1.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/big.js/-/big.js-6.1.6.tgz",
|
|
||||||
"integrity": "sha512-0r9J+Zz9rYm2hOTwiMAVkm3XFQ4u5uTK37xrQMhc9bysn/sf/okzovWMYYIBMFTn/yrEZ11pusgLEaoarTlQbA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/body-parser": {
|
"node_modules/@types/body-parser": {
|
||||||
"version": "1.19.2",
|
"version": "1.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
|
||||||
@@ -3436,18 +3428,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
|
||||||
"integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg=="
|
"integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg=="
|
||||||
},
|
},
|
||||||
"node_modules/big.js": {
|
|
||||||
"version": "6.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.1.tgz",
|
|
||||||
"integrity": "sha512-bCtHMwL9LeDIozFn+oNhhFoq+yQ3BNdnsLSASUxLciOb1vgvpHsIO1dsENiGMgbb4SkP5TrzWzRiLddn8ahVOQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/bigjs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||||
@@ -13504,12 +13484,6 @@
|
|||||||
"@babel/types": "^7.20.7"
|
"@babel/types": "^7.20.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/big.js": {
|
|
||||||
"version": "6.1.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/big.js/-/big.js-6.1.6.tgz",
|
|
||||||
"integrity": "sha512-0r9J+Zz9rYm2hOTwiMAVkm3XFQ4u5uTK37xrQMhc9bysn/sf/okzovWMYYIBMFTn/yrEZ11pusgLEaoarTlQbA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/body-parser": {
|
"@types/body-parser": {
|
||||||
"version": "1.19.2",
|
"version": "1.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
|
||||||
@@ -14498,11 +14472,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
|
||||||
"integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg=="
|
"integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg=="
|
||||||
},
|
},
|
||||||
"big.js": {
|
|
||||||
"version": "6.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.1.tgz",
|
|
||||||
"integrity": "sha512-bCtHMwL9LeDIozFn+oNhhFoq+yQ3BNdnsLSASUxLciOb1vgvpHsIO1dsENiGMgbb4SkP5TrzWzRiLddn8ahVOQ=="
|
|
||||||
},
|
|
||||||
"binary-extensions": {
|
"binary-extensions": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
"@nestjs/schedule": "^3.0.1",
|
"@nestjs/schedule": "^3.0.1",
|
||||||
"@nestjs/typeorm": "^10.0.0",
|
"@nestjs/typeorm": "^10.0.0",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
"big.js": "^6.2.1",
|
|
||||||
"bitcoin-address-validation": "^2.2.1",
|
"bitcoin-address-validation": "^2.2.1",
|
||||||
"bitcoinjs-lib": "^6.1.3",
|
"bitcoinjs-lib": "^6.1.3",
|
||||||
"bitcoinjs-message": "^2.2.0",
|
"bitcoinjs-message": "^2.2.0",
|
||||||
@@ -55,7 +54,6 @@
|
|||||||
"@nestjs/cli": "^9.0.0",
|
"@nestjs/cli": "^9.0.0",
|
||||||
"@nestjs/schematics": "^9.0.0",
|
"@nestjs/schematics": "^9.0.0",
|
||||||
"@nestjs/testing": "^9.0.0",
|
"@nestjs/testing": "^9.0.0",
|
||||||
"@types/big.js": "^6.1.6",
|
|
||||||
"@types/cron": "^2.0.1",
|
"@types/cron": "^2.0.1",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/jest": "29.5.1",
|
"@types/jest": "29.5.1",
|
||||||
|
|||||||
@@ -34,6 +34,24 @@ export class AddressSettingsService {
|
|||||||
return await this.addressSettingsRepository.update({ address }, { bestDifficulty, bestDifficultyUserAgent });
|
return await this.addressSettingsRepository.update({ address }, { bestDifficulty, bestDifficultyUserAgent });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async updateBestDifficultyIfHigher(address: string, bestDifficulty: number, bestDifficultyUserAgent: string) {
|
||||||
|
await this.addressSettingsRepository
|
||||||
|
.createQueryBuilder()
|
||||||
|
.insert()
|
||||||
|
.into(AddressSettingsEntity)
|
||||||
|
.values({ address })
|
||||||
|
.orIgnore()
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return await this.addressSettingsRepository
|
||||||
|
.createQueryBuilder()
|
||||||
|
.update(AddressSettingsEntity)
|
||||||
|
.set({ bestDifficulty, bestDifficultyUserAgent })
|
||||||
|
.where('address = :address', { address })
|
||||||
|
.andWhere('"bestDifficulty" < :bestDifficulty', { bestDifficulty })
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
public async createNew(address: string) {
|
public async createNew(address: string) {
|
||||||
return await this.addressSettingsRepository.save({ address });
|
return await this.addressSettingsRepository.save({ address });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,6 +140,17 @@ export class ClientService {
|
|||||||
public async updateBestDifficulty(id: string, bestDifficulty: number) {
|
public async updateBestDifficulty(id: string, bestDifficulty: number) {
|
||||||
return await this.clientRepository.update({ id }, { bestDifficulty });
|
return await this.clientRepository.update({ id }, { bestDifficulty });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async updateBestDifficultyIfHigher(id: string, bestDifficulty: number) {
|
||||||
|
return await this.clientRepository
|
||||||
|
.createQueryBuilder()
|
||||||
|
.update(ClientEntity)
|
||||||
|
.set({ bestDifficulty })
|
||||||
|
.where('id = :id', { id })
|
||||||
|
.andWhere('"bestDifficulty" < :bestDifficulty', { bestDifficulty })
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
public async connectedClientCount(): Promise<number> {
|
public async connectedClientCount(): Promise<number> {
|
||||||
return await this.clientRepository.count();
|
return await this.clientRepository.count();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,33 @@ describe('MiningJob', () => {
|
|||||||
expect(updatedBlock.version).toBe(jobTemplate.block.version);
|
expect(updatedBlock.version).toBe(jobTemplate.block.version);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should build the same header as the full block update path', () => {
|
||||||
|
const versionMask = parseInt('00002000', 16);
|
||||||
|
const nonce = parseInt('ed460d91', 16);
|
||||||
|
const extraNonce1 = '57a6f098';
|
||||||
|
const extraNonce2 = 'c708000000000000';
|
||||||
|
const timestamp = parseInt(MockRecording1.TIME, 16);
|
||||||
|
|
||||||
|
const updatedBlock = job.copyAndUpdateBlock(
|
||||||
|
jobTemplate,
|
||||||
|
versionMask,
|
||||||
|
nonce,
|
||||||
|
extraNonce1,
|
||||||
|
extraNonce2,
|
||||||
|
timestamp
|
||||||
|
);
|
||||||
|
const fastHeader = job.buildHeaderBuffer(
|
||||||
|
jobTemplate,
|
||||||
|
versionMask,
|
||||||
|
nonce,
|
||||||
|
extraNonce1,
|
||||||
|
extraNonce2,
|
||||||
|
timestamp
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fastHeader.equals(updatedBlock.toBuffer(true))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('should expose coinbase helpers for downstream validation', () => {
|
it('should expose coinbase helpers for downstream validation', () => {
|
||||||
const notify = JSON.parse(job.response(jobTemplate));
|
const notify = JSON.parse(job.response(jobTemplate));
|
||||||
|
|
||||||
|
|||||||
+37
-6
@@ -18,6 +18,9 @@ export class MiningJob {
|
|||||||
private coinbaseTransaction: bitcoinjs.Transaction;
|
private coinbaseTransaction: bitcoinjs.Transaction;
|
||||||
private coinbasePart1: string;
|
private coinbasePart1: string;
|
||||||
private coinbasePart2: string;
|
private coinbasePart2: string;
|
||||||
|
private coinbasePart1Buffer: Buffer;
|
||||||
|
private coinbasePart2Buffer: Buffer;
|
||||||
|
private merkleBranchBuffers: Buffer[];
|
||||||
|
|
||||||
public jobTemplateId: string;
|
public jobTemplateId: string;
|
||||||
public networkDifficulty: number;
|
public networkDifficulty: number;
|
||||||
@@ -34,6 +37,7 @@ export class MiningJob {
|
|||||||
|
|
||||||
this.creation = new Date().getTime();
|
this.creation = new Date().getTime();
|
||||||
this.jobTemplateId = jobTemplate.blockData.id;
|
this.jobTemplateId = jobTemplate.blockData.id;
|
||||||
|
this.merkleBranchBuffers = jobTemplate.merkle_branch.map(branch => Buffer.from(branch, 'hex'));
|
||||||
|
|
||||||
this.coinbaseTransaction = this.createCoinbaseTransaction(payoutInformation, jobTemplate.blockData.coinbasevalue);
|
this.coinbaseTransaction = this.createCoinbaseTransaction(payoutInformation, jobTemplate.blockData.coinbasevalue);
|
||||||
|
|
||||||
@@ -83,6 +87,8 @@ export class MiningJob {
|
|||||||
|
|
||||||
this.coinbasePart1 = serializedCoinbaseTx.slice(0, partOneIndex - (TOTAL_EXTRANONCE_SIZE_BYTES * 2));
|
this.coinbasePart1 = serializedCoinbaseTx.slice(0, partOneIndex - (TOTAL_EXTRANONCE_SIZE_BYTES * 2));
|
||||||
this.coinbasePart2 = serializedCoinbaseTx.slice(partOneIndex);
|
this.coinbasePart2 = serializedCoinbaseTx.slice(partOneIndex);
|
||||||
|
this.coinbasePart1Buffer = Buffer.from(this.coinbasePart1, 'hex');
|
||||||
|
this.coinbasePart2Buffer = Buffer.from(this.coinbasePart2, 'hex');
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -93,17 +99,42 @@ export class MiningJob {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getCoinbasePrefixBuffer(): Buffer {
|
public getCoinbasePrefixBuffer(): Buffer {
|
||||||
return Buffer.from(this.coinbasePart1, 'hex');
|
return Buffer.from(this.coinbasePart1Buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCoinbaseSuffixBuffer(): Buffer {
|
public getCoinbaseSuffixBuffer(): Buffer {
|
||||||
return Buffer.from(this.coinbasePart2, 'hex');
|
return Buffer.from(this.coinbasePart2Buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public cloneCoinbaseTransaction(): bitcoinjs.Transaction {
|
public cloneCoinbaseTransaction(): bitcoinjs.Transaction {
|
||||||
return bitcoinjs.Transaction.fromBuffer(this.coinbaseTransaction.toBuffer());
|
return bitcoinjs.Transaction.fromBuffer(this.coinbaseTransaction.toBuffer());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public buildHeaderBuffer(jobTemplate: IJobTemplate, versionMask: number, nonce: number, extraNonce: string, extraNonce2: string, timestamp: number): Buffer {
|
||||||
|
const coinbaseBuffer = Buffer.concat([
|
||||||
|
this.coinbasePart1Buffer,
|
||||||
|
Buffer.from(`${extraNonce}${extraNonce2}`, 'hex'),
|
||||||
|
this.coinbasePart2Buffer,
|
||||||
|
]);
|
||||||
|
const coinbaseHash = bitcoinjs.crypto.hash256(coinbaseBuffer);
|
||||||
|
const merkleRoot = this.calculateMerkleRootHash(coinbaseHash, this.merkleBranchBuffers);
|
||||||
|
|
||||||
|
let version = jobTemplate.block.version;
|
||||||
|
if (versionMask !== undefined && versionMask != 0) {
|
||||||
|
version = version ^ versionMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
const header = Buffer.alloc(80);
|
||||||
|
header.writeInt32LE(version, 0);
|
||||||
|
jobTemplate.block.prevHash.copy(header, 4);
|
||||||
|
merkleRoot.copy(header, 36);
|
||||||
|
header.writeUInt32LE(timestamp, 68);
|
||||||
|
header.writeUInt32LE(jobTemplate.block.bits, 72);
|
||||||
|
header.writeUInt32LE(nonce, 76);
|
||||||
|
|
||||||
|
return header;
|
||||||
|
}
|
||||||
|
|
||||||
public copyAndUpdateBlock(jobTemplate: IJobTemplate, 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 = Object.assign(new bitcoinjs.Block(), jobTemplate.block);
|
const testBlock = Object.assign(new bitcoinjs.Block(), jobTemplate.block);
|
||||||
@@ -111,7 +142,7 @@ export class MiningJob {
|
|||||||
return Object.assign(new bitcoinjs.Transaction(), tx);
|
return Object.assign(new bitcoinjs.Transaction(), tx);
|
||||||
});
|
});
|
||||||
|
|
||||||
testBlock.transactions[0] = this.coinbaseTransaction;
|
testBlock.transactions[0] = this.cloneCoinbaseTransaction();
|
||||||
|
|
||||||
testBlock.nonce = nonce;
|
testBlock.nonce = nonce;
|
||||||
|
|
||||||
@@ -126,7 +157,7 @@ export class MiningJob {
|
|||||||
testBlock.transactions[0].ins[0].script = Buffer.from(`${nonceScript.substring(0, nonceScript.length - (TOTAL_EXTRANONCE_SIZE_BYTES * 2))}${extraNonce}${extraNonce2}`, 'hex');
|
testBlock.transactions[0].ins[0].script = Buffer.from(`${nonceScript.substring(0, nonceScript.length - (TOTAL_EXTRANONCE_SIZE_BYTES * 2))}${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), jobTemplate.merkle_branch);
|
testBlock.merkleRoot = this.calculateMerkleRootHash(testBlock.transactions[0].getHash(false), this.merkleBranchBuffers);
|
||||||
|
|
||||||
|
|
||||||
testBlock.timestamp = timestamp;
|
testBlock.timestamp = timestamp;
|
||||||
@@ -135,14 +166,14 @@ export class MiningJob {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private calculateMerkleRootHash(newRoot: Buffer, merkleBranches: string[]): Buffer {
|
private calculateMerkleRootHash(newRoot: Buffer, merkleBranches: Buffer[]): Buffer {
|
||||||
|
|
||||||
const bothMerkles = Buffer.alloc(64);
|
const bothMerkles = Buffer.alloc(64);
|
||||||
|
|
||||||
bothMerkles.set(newRoot);
|
bothMerkles.set(newRoot);
|
||||||
|
|
||||||
for (let i = 0; i < merkleBranches.length; i++) {
|
for (let i = 0; i < merkleBranches.length; i++) {
|
||||||
bothMerkles.set(Buffer.from(merkleBranches[i], 'hex'), 32);
|
bothMerkles.set(merkleBranches[i], 32);
|
||||||
newRoot = bitcoinjs.crypto.hash256(bothMerkles);
|
newRoot = bitcoinjs.crypto.hash256(bothMerkles);
|
||||||
bothMerkles.set(newRoot);
|
bothMerkles.set(newRoot);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ import { BitcoinRpcService as MockBitcoinRpcService } from '../services/bitcoin-
|
|||||||
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 { IBlockTemplate } from './bitcoin-rpc/IBlockTemplate';
|
import { IBlockTemplate } from './bitcoin-rpc/IBlockTemplate';
|
||||||
|
import { MiningJob } from './MiningJob';
|
||||||
import { effectiveJobDifficulty, StratumV1Client } from './StratumV1Client';
|
import { effectiveJobDifficulty, StratumV1Client } from './StratumV1Client';
|
||||||
|
import { MiningSubmitMessage } from './stratum-messages/MiningSubmitMessage';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -263,6 +265,7 @@ describe('StratumV1Client', () => {
|
|||||||
jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true));
|
jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true));
|
||||||
|
|
||||||
emitMessage(MockRecording1.MINING_SUBSCRIBE);
|
emitMessage(MockRecording1.MINING_SUBSCRIBE);
|
||||||
|
emitMessage(`{"id": 4, "method": "mining.suggest_difficulty", "params": [0]}`);
|
||||||
emitMessage(MockRecording1.MINING_AUTHORIZE);
|
emitMessage(MockRecording1.MINING_AUTHORIZE);
|
||||||
await new Promise((r) => setTimeout(r, 100));
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
emitMessage(MockRecording1.MINING_SUBMIT);
|
emitMessage(MockRecording1.MINING_SUBMIT);
|
||||||
@@ -312,6 +315,79 @@ describe('StratumV1Client', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should use the header-only fast path for non-block submissions', async () => {
|
||||||
|
jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true));
|
||||||
|
const buildHeaderSpy = jest.spyOn(MiningJob.prototype, 'buildHeaderBuffer');
|
||||||
|
const fullBlockSpy = jest.spyOn(MiningJob.prototype, 'copyAndUpdateBlock');
|
||||||
|
|
||||||
|
emitMessage(MockRecording1.MINING_SUBSCRIBE);
|
||||||
|
emitMessage(`{"id": 4, "method": "mining.suggest_difficulty", "params": [0]}`);
|
||||||
|
emitMessage(MockRecording1.MINING_AUTHORIZE);
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
|
||||||
|
emitMessage(MockRecording1.MINING_SUBMIT);
|
||||||
|
jest.useRealTimers();
|
||||||
|
await new Promise((r) => setTimeout(r, 1000));
|
||||||
|
|
||||||
|
expect(buildHeaderSpy).toHaveBeenCalled();
|
||||||
|
expect(fullBlockSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write accepted response before share accounting finishes', async () => {
|
||||||
|
jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true));
|
||||||
|
|
||||||
|
emitMessage(MockRecording1.MINING_SUBSCRIBE);
|
||||||
|
emitMessage(`{"id": 4, "method": "mining.suggest_difficulty", "params": [0]}`);
|
||||||
|
emitMessage(MockRecording1.MINING_AUTHORIZE);
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
|
||||||
|
let finishAccounting: () => void;
|
||||||
|
const accountingPromise = new Promise<void>((resolve) => {
|
||||||
|
finishAccounting = resolve;
|
||||||
|
});
|
||||||
|
jest.spyOn((client as any).statistics, 'addShares').mockReturnValue(accountingPromise);
|
||||||
|
|
||||||
|
emitMessage(MockRecording1.MINING_SUBMIT);
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect((client as any).write).toHaveBeenCalledWith(`{"id":5,"error":null,"result":true}\n`);
|
||||||
|
|
||||||
|
finishAccounting();
|
||||||
|
jest.useRealTimers();
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update address best difficulty through the atomic path', async () => {
|
||||||
|
jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true));
|
||||||
|
jest.spyOn(client as any, 'calculateDifficulty').mockReturnValue({
|
||||||
|
submissionDifficulty: 1024,
|
||||||
|
submissionHash: 'share'
|
||||||
|
});
|
||||||
|
const addressSettings = moduleRef.get<AddressSettingsService>(AddressSettingsService);
|
||||||
|
const getSettingsSpy = jest.spyOn(addressSettings, 'getSettings');
|
||||||
|
const updateIfHigherSpy = jest.spyOn(addressSettings as any, 'updateBestDifficultyIfHigher').mockResolvedValue({ affected: 1 });
|
||||||
|
const clientUpdateIfHigherSpy = jest.spyOn(clientService as any, 'updateBestDifficultyIfHigher').mockResolvedValue({ affected: 1 });
|
||||||
|
|
||||||
|
emitMessage(MockRecording1.MINING_SUBSCRIBE);
|
||||||
|
emitMessage(`{"id": 4, "method": "mining.suggest_difficulty", "params": [0]}`);
|
||||||
|
emitMessage(MockRecording1.MINING_AUTHORIZE);
|
||||||
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
|
||||||
|
emitMessage(MockRecording1.MINING_SUBMIT);
|
||||||
|
jest.useRealTimers();
|
||||||
|
await new Promise((r) => setTimeout(r, 1000));
|
||||||
|
|
||||||
|
expect(updateIfHigherSpy).toHaveBeenCalledWith(
|
||||||
|
'tb1qumezefzdeqqwn5zfvgdrhxjzc5ylr39uhuxcz4',
|
||||||
|
1024,
|
||||||
|
expect.any(String)
|
||||||
|
);
|
||||||
|
expect(clientUpdateIfHigherSpy).toHaveBeenCalledWith(expect.any(String), 1024);
|
||||||
|
expect(getSettingsSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('should reject duplicate submissions', async () => {
|
it('should reject duplicate submissions', async () => {
|
||||||
jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true));
|
jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true));
|
||||||
|
|
||||||
@@ -330,6 +406,7 @@ describe('StratumV1Client', () => {
|
|||||||
|
|
||||||
it('should reject submissions for unknown jobs', async () => {
|
it('should reject submissions for unknown jobs', async () => {
|
||||||
jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true));
|
jest.spyOn(client as any, 'write').mockImplementation((data) => Promise.resolve(true));
|
||||||
|
const hashSpy = jest.spyOn(MiningSubmitMessage.prototype, 'hash');
|
||||||
|
|
||||||
emitMessage(MockRecording1.MINING_SUBSCRIBE);
|
emitMessage(MockRecording1.MINING_SUBSCRIBE);
|
||||||
emitMessage(`{"id": 4, "method": "mining.suggest_difficulty", "params": [0]}`);
|
emitMessage(`{"id": 4, "method": "mining.suggest_difficulty", "params": [0]}`);
|
||||||
@@ -340,6 +417,8 @@ describe('StratumV1Client', () => {
|
|||||||
await new Promise((r) => setTimeout(r, 100));
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
|
||||||
expect((client as any).write).lastCalledWith(`{"id":5,"result":null,"error":[21,"Job not found",""]}\n`);
|
expect((client as any).write).lastCalledWith(`{"id":5,"result":null,"error":[21,"Job not found",""]}\n`);
|
||||||
|
expect(await clientService.connectedClientCount()).toBe(0);
|
||||||
|
expect(hashSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject submissions when the job template has expired', async () => {
|
it('should reject submissions when the job template has expired', async () => {
|
||||||
@@ -369,6 +448,7 @@ describe('StratumV1Client', () => {
|
|||||||
await new Promise((r) => setTimeout(r, 100));
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
|
|
||||||
expect((client as any).write).lastCalledWith(`{"id":5,"result":null,"error":[23,"Difficulty too low",""]}\n`);
|
expect((client as any).write).lastCalledWith(`{"id":5,"result":null,"error":[23,"Difficulty too low",""]}\n`);
|
||||||
|
expect(await clientService.connectedClientCount()).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject submissions with short extranonce2 values', async () => {
|
it('should reject submissions with short extranonce2 values', async () => {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import Big from 'big.js';
|
|
||||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||||
import { plainToInstance } from 'class-transformer';
|
import { plainToInstance } from 'class-transformer';
|
||||||
import { validate, ValidatorOptions } from 'class-validator';
|
import { validate, ValidatorOptions } from 'class-validator';
|
||||||
@@ -29,6 +28,8 @@ import { EXTRANONCE1_SIZE_BYTES } from './stratum.constants';
|
|||||||
import { SuggestDifficulty } from './stratum-messages/SuggestDifficultyMessage';
|
import { SuggestDifficulty } from './stratum-messages/SuggestDifficultyMessage';
|
||||||
import { StratumV1ClientStatistics } from './StratumV1ClientStatistics';
|
import { StratumV1ClientStatistics } from './StratumV1ClientStatistics';
|
||||||
|
|
||||||
|
const TRUE_DIFF_ONE = 2.695953529101131e67;
|
||||||
|
|
||||||
export function effectiveJobDifficulty(
|
export function effectiveJobDifficulty(
|
||||||
jobIdInt: number,
|
jobIdInt: number,
|
||||||
currentDiff: number,
|
currentDiff: number,
|
||||||
@@ -481,12 +482,13 @@ export class StratumV1Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async handleMiningSubmission(submission: MiningSubmitMessage) {
|
private async ensureClientEntity() {
|
||||||
|
if (this.clientEntity != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.clientEntity == null) {
|
|
||||||
if (this.creatingEntity == null) {
|
if (this.creatingEntity == null) {
|
||||||
this.creatingEntity = new Promise(async (resolve, reject) => {
|
this.creatingEntity = (async () => {
|
||||||
try {
|
|
||||||
this.clientEntity = await this.clientService.insert({
|
this.clientEntity = await this.clientService.insert({
|
||||||
sessionId: this.extraNonceAndSessionId,
|
sessionId: this.extraNonceAndSessionId,
|
||||||
address: this.clientAuthorization.address,
|
address: this.clientAuthorization.address,
|
||||||
@@ -495,33 +497,14 @@ export class StratumV1Client {
|
|||||||
startTime: new Date(),
|
startTime: new Date(),
|
||||||
bestDifficulty: 0
|
bestDifficulty: 0
|
||||||
});
|
});
|
||||||
} catch (e) {
|
})();
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
await this.creatingEntity;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
await this.creatingEntity;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const submissionHash = submission.hash();
|
await this.creatingEntity;
|
||||||
if(this.miningSubmissionHashes.has(submissionHash)){
|
|
||||||
const err = new StratumErrorMessage(
|
|
||||||
submission.id,
|
|
||||||
eStratumErrorCode.DuplicateShare,
|
|
||||||
'Duplicate share').response();
|
|
||||||
const success = await this.write(err);
|
|
||||||
if (!success) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}else{
|
|
||||||
this.miningSubmissionHashes.add(submissionHash);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handleMiningSubmission(submission: MiningSubmitMessage) {
|
||||||
|
|
||||||
const job = this.stratumV1JobsService.getJobById(submission.jobId);
|
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 (or expired, 5 min)
|
// a miner may submit a job that doesn't exist anymore if it was removed by a new block notification (or expired, 5 min)
|
||||||
@@ -566,16 +549,39 @@ export class StratumV1Client {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const submissionHash = [
|
||||||
|
submission.jobId,
|
||||||
|
submission.extraNonce2,
|
||||||
|
submission.ntime,
|
||||||
|
submission.nonce,
|
||||||
|
submission.versionMask ?? ''
|
||||||
|
].join(':');
|
||||||
|
if (this.miningSubmissionHashes.has(submissionHash)) {
|
||||||
|
const err = new StratumErrorMessage(
|
||||||
|
submission.id,
|
||||||
|
eStratumErrorCode.DuplicateShare,
|
||||||
|
'Duplicate share').response();
|
||||||
|
const success = await this.write(err);
|
||||||
|
if (!success) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
this.miningSubmissionHashes.add(submissionHash);
|
||||||
|
}
|
||||||
|
|
||||||
const updatedJobBlock = job.copyAndUpdateBlock(
|
const versionMask = parseInt(submission.versionMask, 16);
|
||||||
|
const nonce = parseInt(submission.nonce, 16);
|
||||||
|
const timestamp = parseInt(submission.ntime, 16);
|
||||||
|
|
||||||
|
const header = job.buildHeaderBuffer(
|
||||||
jobTemplate,
|
jobTemplate,
|
||||||
parseInt(submission.versionMask, 16),
|
versionMask,
|
||||||
parseInt(submission.nonce, 16),
|
nonce,
|
||||||
this.extraNonceAndSessionId,
|
this.extraNonceAndSessionId,
|
||||||
submission.extraNonce2,
|
submission.extraNonce2,
|
||||||
parseInt(submission.ntime, 16)
|
timestamp
|
||||||
);
|
);
|
||||||
const header = updatedJobBlock.toBuffer(true);
|
|
||||||
const { submissionDifficulty } = this.calculateDifficulty(header);
|
const { submissionDifficulty } = 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}`);
|
||||||
@@ -588,9 +594,21 @@ export class StratumV1Client {
|
|||||||
|
|
||||||
|
|
||||||
if (submissionDifficulty >= effectiveDiff) {
|
if (submissionDifficulty >= effectiveDiff) {
|
||||||
|
const success = await this.write(JSON.stringify(submission.response()) + '\n');
|
||||||
|
if (!success) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (submissionDifficulty >= jobTemplate.blockData.networkDifficulty) {
|
if (submissionDifficulty >= jobTemplate.blockData.networkDifficulty) {
|
||||||
console.log('!!! BLOCK FOUND !!!');
|
console.log('!!! BLOCK FOUND !!!');
|
||||||
|
const updatedJobBlock = job.copyAndUpdateBlock(
|
||||||
|
jobTemplate,
|
||||||
|
versionMask,
|
||||||
|
nonce,
|
||||||
|
this.extraNonceAndSessionId,
|
||||||
|
submission.extraNonce2,
|
||||||
|
timestamp
|
||||||
|
);
|
||||||
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);
|
||||||
if (result === 'SUCCESS!') {
|
if (result === 'SUCCESS!') {
|
||||||
@@ -608,12 +626,13 @@ export class StratumV1Client {
|
|||||||
console.warn(`[Block submit rejected at height ${jobTemplate.blockData.height}]: ${result}`);
|
console.warn(`[Block submit rejected at height ${jobTemplate.blockData.height}]: ${result}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await this.ensureClientEntity();
|
||||||
try {
|
try {
|
||||||
await this.statistics.addShares(this.clientEntity, effectiveDiff);
|
await this.statistics.addShares(this.clientEntity, effectiveDiff);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
// only update every minute
|
// only update every minute
|
||||||
//if (this.clientEntity.updatedAt == null || now.getTime() - this.clientEntity.updatedAt.getTime() > 1000 * 60) {
|
//if (this.clientEntity.updatedAt == null || now.getTime() - this.clientEntity.updatedAt.getTime() > 1000 * 60) {
|
||||||
await this.clientService.heartbeatBulkAsync(this.clientEntity.id, this.statistics.hashRate, now);
|
this.clientService.heartbeatBulkAsync(this.clientEntity.id, this.statistics.hashRate, now);
|
||||||
this.clientEntity.updatedAt = now;
|
this.clientEntity.updatedAt = now;
|
||||||
//}
|
//}
|
||||||
|
|
||||||
@@ -622,11 +641,9 @@ export class StratumV1Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (submissionDifficulty > this.clientEntity.bestDifficulty) {
|
if (submissionDifficulty > this.clientEntity.bestDifficulty) {
|
||||||
await this.clientService.updateBestDifficulty(this.clientEntity.id, submissionDifficulty);
|
await this.clientService.updateBestDifficultyIfHigher(this.clientEntity.id, submissionDifficulty);
|
||||||
this.clientEntity.bestDifficulty = submissionDifficulty;
|
this.clientEntity.bestDifficulty = submissionDifficulty;
|
||||||
if (submissionDifficulty > (await this.addressSettingsService.getSettings(this.clientAuthorization.address, true)).bestDifficulty) {
|
await this.addressSettingsService.updateBestDifficultyIfHigher(this.clientAuthorization.address, submissionDifficulty, this.clientEntity.userAgent);
|
||||||
await this.addressSettingsService.updateBestDifficulty(this.clientAuthorization.address, submissionDifficulty, this.clientEntity.userAgent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -646,7 +663,7 @@ export class StratumV1Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//await this.checkDifficulty();
|
//await this.checkDifficulty();
|
||||||
return true;
|
return false;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,20 +706,18 @@ export class StratumV1Client {
|
|||||||
|
|
||||||
const hashResult = bitcoinjs.crypto.hash256(header);
|
const hashResult = bitcoinjs.crypto.hash256(header);
|
||||||
|
|
||||||
let s64 = this.le256todouble(hashResult);
|
const target = this.le256todouble(hashResult);
|
||||||
|
const submissionDifficulty = target === 0 ? Number.POSITIVE_INFINITY : TRUE_DIFF_ONE / target;
|
||||||
const truediffone = Big('26959535291011309493156476344723991336010898738574164086137773096960');
|
return { submissionDifficulty, submissionHash: hashResult.toString('hex') };
|
||||||
const difficulty = truediffone.div(s64.toString());
|
|
||||||
return { submissionDifficulty: difficulty.toNumber(), submissionHash: hashResult.toString('hex') };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private le256todouble(target: Buffer): bigint {
|
private le256todouble(target: Buffer): number {
|
||||||
|
|
||||||
const number = target.reduceRight((acc, byte) => {
|
let number = 0;
|
||||||
// Shift the number 8 bits to the left and OR with the current byte
|
for (let i = target.length - 1; i >= 0; i--) {
|
||||||
return (acc << BigInt(8)) | BigInt(byte);
|
number = number * 256 + target[i];
|
||||||
}, BigInt(0));
|
}
|
||||||
|
|
||||||
return number;
|
return number;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user