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:
Stijn 2024-08-30 14:57:43 +02:00 committed by GitHub
parent 7bb436bf1f
commit 8ce057b3b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 209 additions and 6 deletions

View File

@ -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"

View 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);
});
});
});

View File

@ -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');

View File

@ -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();

View File

@ -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,