mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2025-03-17 13:21:43 +01:00
Add pool identifier configuration and improve script size handling (#60)
* fixed a couple tests because of imports, missing newline chars and missing constructor parameters * Add pool identifier configuration and improve script size handling
This commit is contained in:
parent
7bb436bf1f
commit
8ce057b3b5
@ -34,3 +34,5 @@ DEV_FEE_ADDRESS=
|
||||
NETWORK=mainnet
|
||||
|
||||
API_SECURE=false
|
||||
# Default is "public-pool", you can change it to any string it will be removed if it will make the block or coinbase script too big
|
||||
POOL_IDENTIFIER="public-pool"
|
||||
|
181
src/models/MiningJob.spec.ts
Normal file
181
src/models/MiningJob.spec.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { MiningJob } from "./MiningJob";
|
||||
import { IJobTemplate } from "../services/stratum-v1-jobs.service";
|
||||
|
||||
|
||||
function hexToAscii(hex: string): string {
|
||||
let ascii = '';
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
const char = String.fromCharCode(parseInt(hex.substr(i, 2), 16));
|
||||
if (char !== '\0') { // Ignore null characters to make sure string matching works
|
||||
ascii += char;
|
||||
}
|
||||
}
|
||||
return ascii;
|
||||
}
|
||||
|
||||
function extractPoolIdentifierFromScript(coinbaseHex: string) {
|
||||
let offset = 8 + 2 + 64 + 8; // Skip Version, Input Count, Previous Transaction Hash, Previous Output Index
|
||||
|
||||
// Coinbase Data Length (1 byte / 2 hex characters)
|
||||
const coinbaseDataLengthHex = coinbaseHex.substr(offset, 2);
|
||||
const coinbaseDataLength = parseInt(coinbaseDataLengthHex, 16);
|
||||
offset += 2;
|
||||
|
||||
// Coinbase Data (coinbaseDataLength * 2 hex characters)
|
||||
const coinbaseDataHex = coinbaseHex.substr(offset, coinbaseDataLength * 2);
|
||||
|
||||
const coinbaseDataAscii = hexToAscii(coinbaseDataHex);
|
||||
|
||||
return coinbaseDataAscii;
|
||||
}
|
||||
|
||||
describe('MiningJob', () => {
|
||||
let moduleRef: TestingModule;
|
||||
let configService: ConfigService;
|
||||
let block: bitcoinjs.Block;
|
||||
let jobTemplate: IJobTemplate;
|
||||
let payoutInformation: any[];
|
||||
|
||||
|
||||
beforeAll(async () => {
|
||||
moduleRef = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: {
|
||||
get: jest.fn((key: string) => {
|
||||
switch (key) {
|
||||
// Configure mock responses for ConfigService
|
||||
}
|
||||
return null;
|
||||
})
|
||||
}
|
||||
}
|
||||
],
|
||||
}).compile();
|
||||
configService = moduleRef.get<ConfigService>(ConfigService);
|
||||
});
|
||||
|
||||
|
||||
describe('constructor', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
console.warn = jest.fn((message: string) => console.log('WARN:', message));
|
||||
payoutInformation = [
|
||||
{
|
||||
address: 'tb1qr2ylpdgp9ejpt6v2uxlqrn9penp82rzz2grnns',
|
||||
percent: 100,
|
||||
}
|
||||
];
|
||||
block = new bitcoinjs.Block();
|
||||
block.witnessCommit = Buffer.from('000');
|
||||
block.prevHash = Buffer.from('000');
|
||||
block.merkleRoot = Buffer.from('000');
|
||||
block.transactions = [];
|
||||
|
||||
const maxTransactions = 7545; // this is only for block weight calculation, other metrics like operations are not considered because they are not affected by the size of the pool identifier
|
||||
for (let i = 0; i < maxTransactions; i++) {
|
||||
block.transactions.push(bitcoinjs.Transaction.fromHex("010000000001017405e391018c5e9dc79f324f9607c9c46d21b02f66dabaa870b4add871d6379f01000000171600148d7a0a3461e3891723e5fdf8129caa0075060cffffffffff01fcf60200000000001600148d7a0a3461e3891723e5fdf8129caa0075060cff0248304502210088025cffdaf69d310c6fed11832edd9c19b6a912c132262701ad0e6133227d9202207d73bbf777abd2aeae995d684e6bb1a048c5ac722e16de48bdd35643df7decf001210283409659355b6d1cc3c32decd5d561abaac86c37a353b52895a5e6c196d6f44800000000"));
|
||||
}
|
||||
|
||||
jobTemplate = {
|
||||
block: block,
|
||||
blockData: {
|
||||
id: '1',
|
||||
coinbasevalue: 0,
|
||||
networkDifficulty: 0,
|
||||
height: 0,
|
||||
clearJobs: false,
|
||||
}
|
||||
} as IJobTemplate;
|
||||
});
|
||||
it('should create a new MiningJob if POOL_IDENTIFIER is not set and use the default', () => {
|
||||
const expectedMiningIdentifier = 'Public-Pool';
|
||||
expect(jobTemplate.block).toBeDefined();
|
||||
const miningJob = new MiningJob(configService, bitcoinjs.networks.testnet, '1', payoutInformation, jobTemplate);
|
||||
|
||||
const response = JSON.parse(miningJob.response(jobTemplate));
|
||||
|
||||
const miningIdentifier = extractPoolIdentifierFromScript(response.params[2]);
|
||||
expect(miningIdentifier).toBe(expectedMiningIdentifier);
|
||||
expect(console.warn).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should use the POOL_IDENTIFIER if it doesn\'t make the script size too big', () => {
|
||||
const expectedMiningIdentifier = 'My Mining Pool';
|
||||
|
||||
configService.get = jest.fn((key: string) => {
|
||||
switch (key) {
|
||||
case 'POOL_IDENTIFIER': return expectedMiningIdentifier;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const miningJob = new MiningJob(configService, bitcoinjs.networks.testnet, '1', payoutInformation, jobTemplate);
|
||||
const response = JSON.parse(miningJob.response(jobTemplate));
|
||||
|
||||
const miningIdentifier = extractPoolIdentifierFromScript(response.params[2]);
|
||||
expect(miningIdentifier).toBe(expectedMiningIdentifier);
|
||||
expect(console.warn).not.toBeCalled();
|
||||
|
||||
});
|
||||
|
||||
it('should remove pool identifier if block is too big with identifier', () => {
|
||||
const expectedMiningIdentifier = '';
|
||||
|
||||
configService.get = jest.fn((key: string) => {
|
||||
switch (key) {
|
||||
case 'POOL_IDENTIFIER': return 'A'.repeat(84);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const miningJob = new MiningJob(configService, bitcoinjs.networks.testnet, '1', payoutInformation, jobTemplate);
|
||||
const response = JSON.parse(miningJob.response(jobTemplate));
|
||||
|
||||
const miningIdentifier = extractPoolIdentifierFromScript(response.params[2]);
|
||||
expect(console.warn).toBeCalledWith('Block weight exceeds the maximum allowed weight, removing the pool identifier');
|
||||
expect(miningIdentifier).toBe(expectedMiningIdentifier);
|
||||
});
|
||||
|
||||
it('should use the POOL_IDENTIFIER if it doesn\'t make the script size too big with identifier abcabc', () => {
|
||||
|
||||
jobTemplate.block.transactions = []; // remove transactions because we only want to test the script size
|
||||
const expectedMiningIdentifier = 'A'.repeat(88); // 88 chars is the maximum size validated against bitcoin core in regtest
|
||||
configService.get = jest.fn((key: string) => {
|
||||
switch (key) {
|
||||
case 'POOL_IDENTIFIER': return expectedMiningIdentifier;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const miningJob = new MiningJob(configService, bitcoinjs.networks.testnet, '1', payoutInformation, jobTemplate);
|
||||
const response = JSON.parse(miningJob.response(jobTemplate));
|
||||
|
||||
const miningIdentifier = extractPoolIdentifierFromScript(response.params[2]);
|
||||
expect(miningIdentifier).toBe(expectedMiningIdentifier);
|
||||
expect(console.warn).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should remove pool identifier if script is too big with identifier', () => {
|
||||
const expectedMiningIdentifier = '';
|
||||
jobTemplate.block.transactions = []; // remove transactions because we only want to test the script size
|
||||
configService.get = jest.fn((key: string) => {
|
||||
switch (key) {
|
||||
case 'POOL_IDENTIFIER': return 'A'.repeat(89); // 88 chars is the maximum size validated against bitcoin core in regtest
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const miningJob = new MiningJob(configService, bitcoinjs.networks.testnet, '1', payoutInformation, jobTemplate);
|
||||
const response = JSON.parse(miningJob.response(jobTemplate));
|
||||
|
||||
const miningIdentifier = extractPoolIdentifierFromScript(response.params[2]);
|
||||
expect(console.warn).toBeCalledWith('Pool identifier is too long, removing the pool identifier');
|
||||
expect(miningIdentifier).toBe(expectedMiningIdentifier);
|
||||
});
|
||||
});
|
||||
});
|
@ -4,8 +4,10 @@ import * as bitcoinjs from 'bitcoinjs-lib';
|
||||
import { IJobTemplate } from '../services/stratum-v1-jobs.service';
|
||||
import { eResponseMethod } from './enums/eResponseMethod';
|
||||
import { IMiningNotify } from './stratum-messages/IMiningNotify';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
|
||||
const MAX_BLOCK_WEIGHT = 4000000;
|
||||
const MAX_SCRIPT_SIZE = 100; // https://github.com/bitcoin/bitcoin/blob/ffdc3d6060f6e65e69cf115a13b83e6eb4a0a0a8/src/consensus/tx_check.cpp#L49
|
||||
interface AddressObject {
|
||||
address: string;
|
||||
percent: number;
|
||||
@ -21,6 +23,7 @@ export class MiningJob {
|
||||
|
||||
|
||||
constructor(
|
||||
configService: ConfigService,
|
||||
private network: bitcoinjs.networks.Network,
|
||||
public jobId: string,
|
||||
payoutInformation: AddressObject[],
|
||||
@ -39,7 +42,9 @@ export class MiningJob {
|
||||
// 32-byte - Commitment hash: Double-SHA256(witness root hash|witness reserved value)
|
||||
|
||||
// 39th byte onwards: Optional data with no consensus meaning
|
||||
const extra = Buffer.from('Public-Pool');
|
||||
// Initial pool identifier
|
||||
let poolIdentifier = configService.get('POOL_IDENTIFIER') || 'Public-Pool';
|
||||
let extra = Buffer.from(poolIdentifier);
|
||||
|
||||
// Encode the block height
|
||||
// https://github.com/bitcoin/bips/blob/master/bip-0034.mediawiki
|
||||
@ -48,14 +53,27 @@ export class MiningJob {
|
||||
// Get the length of the block height encoding
|
||||
const blockHeightLengthByte = Buffer.from([blockHeightEncoded.length]);
|
||||
|
||||
// generate padding and take length of encode blockHeight into account
|
||||
// Generate padding and take length of encode blockHeight into account
|
||||
const padding = Buffer.alloc(8 + (3 - blockHeightEncoded.length), 0)
|
||||
|
||||
// build the script
|
||||
this.coinbaseTransaction.ins[0].script = Buffer.concat([blockHeightLengthByte, blockHeightEncoded, extra, padding])
|
||||
// Build the script
|
||||
let script = Buffer.concat([blockHeightLengthByte, blockHeightEncoded, extra, padding]);
|
||||
// Check if the pool identifier is too long
|
||||
if (script.length > MAX_SCRIPT_SIZE) {
|
||||
console.warn('Pool identifier is too long, removing the pool identifier');
|
||||
script = Buffer.concat([blockHeightLengthByte, blockHeightEncoded, padding]);
|
||||
}
|
||||
|
||||
this.coinbaseTransaction.ins[0].script = script;
|
||||
this.coinbaseTransaction.addOutput(bitcoinjs.script.compile([bitcoinjs.opcodes.OP_RETURN, Buffer.concat([segwitMagicBits, jobTemplate.block.witnessCommit])]), 0);
|
||||
|
||||
// Check if the pool identifier is too long
|
||||
if ((this.coinbaseTransaction.weight() + jobTemplate.block.weight()) > MAX_BLOCK_WEIGHT) {
|
||||
console.warn('Block weight exceeds the maximum allowed weight, removing the pool identifier');
|
||||
let script = Buffer.concat([blockHeightLengthByte, blockHeightEncoded, padding]);
|
||||
this.coinbaseTransaction.ins[0].script = script;
|
||||
}
|
||||
|
||||
// get the non-witness coinbase tx
|
||||
//@ts-ignore
|
||||
const serializedCoinbaseTx = this.coinbaseTransaction.__toBuffer().toString('hex');
|
||||
|
@ -108,6 +108,7 @@ describe('StratumV1Client', () => {
|
||||
|
||||
configService = moduleRef.get<ConfigService>(ConfigService);
|
||||
|
||||
|
||||
bitcoinRpcService = new MockBitcoinRpcService(configService,null);
|
||||
jest.spyOn(bitcoinRpcService, 'getBlockTemplate').mockReturnValue(Promise.resolve(MockRecording1.BLOCK_TEMPLATE));
|
||||
bitcoinRpcService.newBlock$ = newBlockEmitter.asObservable();
|
||||
|
@ -53,7 +53,7 @@ export class StratumV1Client {
|
||||
public hashRate: number = 0;
|
||||
|
||||
private buffer: string = '';
|
||||
|
||||
|
||||
constructor(
|
||||
public readonly socket: Socket,
|
||||
private readonly stratumV1JobsService: StratumV1JobsService,
|
||||
@ -418,6 +418,7 @@ export class StratumV1Client {
|
||||
}
|
||||
|
||||
const job = new MiningJob(
|
||||
this.configService,
|
||||
network,
|
||||
this.stratumV1JobsService.getNextId(),
|
||||
payoutInformation,
|
||||
|
Loading…
x
Reference in New Issue
Block a user