mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 09:05:06 -07: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:
@@ -34,3 +34,5 @@ DEV_FEE_ADDRESS=
|
|||||||
NETWORK=mainnet
|
NETWORK=mainnet
|
||||||
|
|
||||||
API_SECURE=false
|
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"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+23
-5
@@ -4,8 +4,10 @@ import * as bitcoinjs from 'bitcoinjs-lib';
|
|||||||
import { IJobTemplate } from '../services/stratum-v1-jobs.service';
|
import { IJobTemplate } from '../services/stratum-v1-jobs.service';
|
||||||
import { eResponseMethod } from './enums/eResponseMethod';
|
import { eResponseMethod } from './enums/eResponseMethod';
|
||||||
import { IMiningNotify } from './stratum-messages/IMiningNotify';
|
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 {
|
interface AddressObject {
|
||||||
address: string;
|
address: string;
|
||||||
percent: number;
|
percent: number;
|
||||||
@@ -21,6 +23,7 @@ export class MiningJob {
|
|||||||
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
configService: ConfigService,
|
||||||
private network: bitcoinjs.networks.Network,
|
private network: bitcoinjs.networks.Network,
|
||||||
public jobId: string,
|
public jobId: string,
|
||||||
payoutInformation: AddressObject[],
|
payoutInformation: AddressObject[],
|
||||||
@@ -39,7 +42,9 @@ export class MiningJob {
|
|||||||
// 32-byte - Commitment hash: Double-SHA256(witness root hash|witness reserved value)
|
// 32-byte - Commitment hash: Double-SHA256(witness root hash|witness reserved value)
|
||||||
|
|
||||||
// 39th byte onwards: Optional data with no consensus meaning
|
// 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
|
// Encode the block height
|
||||||
// https://github.com/bitcoin/bips/blob/master/bip-0034.mediawiki
|
// 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
|
// Get the length of the block height encoding
|
||||||
const blockHeightLengthByte = Buffer.from([blockHeightEncoded.length]);
|
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)
|
const padding = Buffer.alloc(8 + (3 - blockHeightEncoded.length), 0)
|
||||||
|
|
||||||
// build the script
|
// Build the script
|
||||||
this.coinbaseTransaction.ins[0].script = Buffer.concat([blockHeightLengthByte, blockHeightEncoded, extra, padding])
|
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);
|
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
|
// get the non-witness coinbase tx
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const serializedCoinbaseTx = this.coinbaseTransaction.__toBuffer().toString('hex');
|
const serializedCoinbaseTx = this.coinbaseTransaction.__toBuffer().toString('hex');
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ describe('StratumV1Client', () => {
|
|||||||
|
|
||||||
configService = moduleRef.get<ConfigService>(ConfigService);
|
configService = moduleRef.get<ConfigService>(ConfigService);
|
||||||
|
|
||||||
|
|
||||||
bitcoinRpcService = new MockBitcoinRpcService(configService,null);
|
bitcoinRpcService = new MockBitcoinRpcService(configService,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();
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export class StratumV1Client {
|
|||||||
public hashRate: number = 0;
|
public hashRate: number = 0;
|
||||||
|
|
||||||
private buffer: string = '';
|
private buffer: string = '';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly socket: Socket,
|
public readonly socket: Socket,
|
||||||
private readonly stratumV1JobsService: StratumV1JobsService,
|
private readonly stratumV1JobsService: StratumV1JobsService,
|
||||||
@@ -418,6 +418,7 @@ export class StratumV1Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const job = new MiningJob(
|
const job = new MiningJob(
|
||||||
|
this.configService,
|
||||||
network,
|
network,
|
||||||
this.stratumV1JobsService.getNextId(),
|
this.stratumV1JobsService.getNextId(),
|
||||||
payoutInformation,
|
payoutInformation,
|
||||||
|
|||||||
Reference in New Issue
Block a user