mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 09:05:06 -07:00
241 lines
9.3 KiB
TypeScript
241 lines
9.3 KiB
TypeScript
import { AddressType, getAddressInfo } from 'bitcoin-address-validation';
|
|
import * as bitcoinjs from 'bitcoinjs-lib';
|
|
import * as crypto from 'crypto';
|
|
import * as merkle from 'merkle-lib';
|
|
import * as merkleProof from 'merkle-lib/proof';
|
|
|
|
import { IBlockTemplate } from './bitcoin-rpc/IBlockTemplate';
|
|
import { eResponseMethod } from './enums/eResponseMethod';
|
|
|
|
interface AddressObject {
|
|
address: string;
|
|
percent: number;
|
|
}
|
|
export class MiningJob {
|
|
|
|
private coinbasePart1: string;
|
|
private coinbasePart2: string;
|
|
|
|
private merkle_branch: string[]; // List of hashes, will be used for calculation of merkle root. This is not a list of all transactions, it only contains prepared hashes of steps of merkle tree algorithm.
|
|
|
|
public jobId: string; // ID of the job. Use this ID while submitting share generated from this job.
|
|
public block: bitcoinjs.Block = new bitcoinjs.Block();
|
|
public networkDifficulty: number;
|
|
|
|
constructor(id: string, payoutInformation: AddressObject[], public blockTemplate: IBlockTemplate, public clean_jobs: boolean) {
|
|
|
|
this.jobId = id;
|
|
this.block.prevHash = this.convertToLittleEndian(blockTemplate.previousblockhash);
|
|
|
|
this.block.version = blockTemplate.version;
|
|
this.block.bits = parseInt(blockTemplate.bits, 16);
|
|
this.networkDifficulty = this.calculateNetworkDifficulty(this.block.bits);
|
|
this.block.timestamp = Math.floor(new Date().getTime() / 1000);
|
|
|
|
this.block.transactions = blockTemplate.transactions.map(t => bitcoinjs.Transaction.fromHex(t.data));
|
|
|
|
const coinbaseTransaction = this.createCoinbaseTransaction(payoutInformation, this.blockTemplate.coinbasevalue);
|
|
this.block.transactions.unshift(coinbaseTransaction);
|
|
|
|
this.block.witnessCommit = bitcoinjs.Block.calculateMerkleRoot(this.block.transactions, true);
|
|
|
|
|
|
|
|
|
|
// https://github.com/bitcoin/bips/blob/master/bip-0034.mediawiki
|
|
//const blockHeightScript = Buffer.from(`03${this.blockTemplate.height.toString(16).padStart(8, '0')}`, 'hex');
|
|
|
|
const littleEndianBlockHeight = this.convertToLittleEndian(this.blockTemplate.height.toString(16).padStart(6, '0'))
|
|
|
|
//The commitment is recorded in a scriptPubKey of the coinbase transaction. It must be at least 38 bytes, with the first 6-byte of 0x6a24aa21a9ed, that is:
|
|
// 1-byte - OP_RETURN (0x6a)
|
|
// 1-byte - Push the following 36 bytes (0x24)
|
|
// 4-byte - Commitment header (0xaa21a9ed)
|
|
const segwitMagicBits = Buffer.from('aa21a9ed', 'hex');
|
|
// 32-byte - Commitment hash: Double-SHA256(witness root hash|witness reserved value)
|
|
|
|
// 39th byte onwards: Optional data with no consensus meaning
|
|
coinbaseTransaction.ins[0].script = Buffer.concat([Buffer.from([littleEndianBlockHeight.byteLength]), littleEndianBlockHeight, Buffer.from('00000000' + '00000000', 'hex')]);
|
|
coinbaseTransaction.addOutput(bitcoinjs.script.compile([bitcoinjs.opcodes.OP_RETURN, Buffer.concat([segwitMagicBits, this.block.witnessCommit])]), 0);
|
|
|
|
// get the non-witness coinbase tx
|
|
//@ts-ignore
|
|
const serializedCoinbaseTx = coinbaseTransaction.__toBuffer().toString('hex');
|
|
|
|
const inputScript = coinbaseTransaction.ins[0].script.toString('hex');
|
|
|
|
const partOneIndex = serializedCoinbaseTx.indexOf(inputScript) + inputScript.length;
|
|
|
|
const coinbasePart1 = serializedCoinbaseTx.slice(0, partOneIndex);
|
|
const coinbasePart2 = serializedCoinbaseTx.slice(partOneIndex);
|
|
this.coinbasePart1 = coinbasePart1.slice(0, coinbasePart1.length - 16);
|
|
this.coinbasePart2 = coinbasePart2;
|
|
|
|
|
|
// Calculate merkle branch
|
|
const transactionBuffers = this.block.transactions.map(tx => tx.getHash(false));
|
|
|
|
const merkleTree = merkle(transactionBuffers, bitcoinjs.crypto.hash256);
|
|
const merkleBranches: Buffer[] = merkleProof(merkleTree, transactionBuffers[0]).filter(h => h != null);
|
|
this.block.merkleRoot = merkleBranches.pop();
|
|
|
|
this.merkle_branch = merkleBranches.slice(1, merkleBranches.length).map(b => b.toString('hex'))
|
|
|
|
this.block.transactions[0] = coinbaseTransaction;
|
|
|
|
}
|
|
|
|
public copyAndUpdateBlock(versionMask: number, nonce: number, extraNonce: string, extraNonce2: string, timestamp: number): bitcoinjs.Block {
|
|
|
|
const testBlock = bitcoinjs.Block.fromBuffer(this.block.toBuffer());
|
|
|
|
testBlock.nonce = nonce;
|
|
|
|
// recompute version mask
|
|
if (versionMask !== undefined && versionMask != 0) {
|
|
testBlock.version = (testBlock.version ^ versionMask);
|
|
}
|
|
|
|
// set the nonces
|
|
const nonceFreeScript = testBlock.transactions[0].ins[0].script.toString('hex');
|
|
|
|
testBlock.transactions[0].ins[0].script = Buffer.from(`${nonceFreeScript.substring(0, nonceFreeScript.length - 16)}${extraNonce}${extraNonce2}`, 'hex');
|
|
|
|
//recompute the roots
|
|
testBlock.merkleRoot = this.calculateMerkleRootHash(testBlock.transactions[0].getHash(false), this.merkle_branch);
|
|
|
|
|
|
testBlock.timestamp = timestamp;
|
|
|
|
return testBlock;
|
|
}
|
|
|
|
private calculateMerkleRootHash(newRoot: Buffer, merkleBranches: string[]): Buffer {
|
|
|
|
const bothMerkles = Buffer.alloc(64);
|
|
|
|
bothMerkles.set(newRoot);
|
|
|
|
for (let i = 0; i < merkleBranches.length; i++) {
|
|
bothMerkles.set(Buffer.from(merkleBranches[i], 'hex'), 32);
|
|
newRoot = this.sha256(this.sha256(bothMerkles));
|
|
bothMerkles.set(newRoot);
|
|
}
|
|
|
|
return bothMerkles.subarray(0, 32)
|
|
}
|
|
|
|
|
|
private createCoinbaseTransaction(addresses: AddressObject[], reward: number): bitcoinjs.Transaction {
|
|
// Part 1
|
|
const coinbaseTransaction = new bitcoinjs.Transaction();
|
|
|
|
// Set the version of the transaction
|
|
coinbaseTransaction.version = 2;
|
|
|
|
// Add the coinbase input (input with no previous output)
|
|
coinbaseTransaction.addInput(Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex'), 0xffffffff, 0xffffffff);
|
|
|
|
// Add an output
|
|
let rewardBalance = reward;
|
|
|
|
addresses.forEach(recipientAddress => {
|
|
const amount = Math.floor((recipientAddress.percent / 100) * reward);
|
|
rewardBalance -= amount;
|
|
coinbaseTransaction.addOutput(this.getPaymentScript(recipientAddress.address), amount);
|
|
})
|
|
|
|
//Add any remaining sats from the Math.floor
|
|
coinbaseTransaction.outs[0].value += rewardBalance;
|
|
|
|
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];
|
|
|
|
return coinbaseTransaction;
|
|
}
|
|
|
|
private getPaymentScript(address: string): Buffer {
|
|
const addressInfo = getAddressInfo(address);
|
|
switch (addressInfo.type) {
|
|
case AddressType.p2wpkh: {
|
|
return bitcoinjs.payments.p2wpkh({ address, network: bitcoinjs.networks.testnet }).output;
|
|
}
|
|
case AddressType.p2pkh: {
|
|
return bitcoinjs.payments.p2pkh({ address }).output;
|
|
}
|
|
case AddressType.p2sh: {
|
|
return bitcoinjs.payments.p2sh({ address }).output;
|
|
}
|
|
case AddressType.p2tr: {
|
|
return bitcoinjs.payments.p2tr({ address }).output;
|
|
}
|
|
case AddressType.p2wsh: {
|
|
return bitcoinjs.payments.p2wsh({ address }).output;
|
|
}
|
|
default: {
|
|
return Buffer.alloc(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
private sha256(data) {
|
|
return crypto.createHash('sha256').update(data).digest()
|
|
}
|
|
|
|
|
|
public response(): string {
|
|
|
|
const job = {
|
|
id: null,
|
|
method: eResponseMethod.MINING_NOTIFY,
|
|
params: [
|
|
this.jobId,
|
|
this.swapEndianWords(this.block.prevHash).toString('hex'),
|
|
this.coinbasePart1,
|
|
this.coinbasePart2,
|
|
this.merkle_branch,
|
|
this.block.version.toString(16),
|
|
this.block.bits.toString(16),
|
|
this.block.timestamp.toString(16),
|
|
this.clean_jobs
|
|
]
|
|
};
|
|
|
|
return JSON.stringify(job) + '\n';
|
|
}
|
|
|
|
|
|
private convertToLittleEndian(hash: string): Buffer {
|
|
const bytes = Buffer.from(hash, 'hex');
|
|
Array.prototype.reverse.call(bytes);
|
|
return bytes;
|
|
}
|
|
|
|
private swapEndianWords(buffer: Buffer): Buffer {
|
|
const swappedBuffer = Buffer.alloc(buffer.length);
|
|
|
|
for (let i = 0; i < buffer.length; i += 4) {
|
|
swappedBuffer[i] = buffer[i + 3];
|
|
swappedBuffer[i + 1] = buffer[i + 2];
|
|
swappedBuffer[i + 2] = buffer[i + 1];
|
|
swappedBuffer[i + 3] = buffer[i];
|
|
}
|
|
|
|
return swappedBuffer;
|
|
}
|
|
|
|
|
|
private calculateNetworkDifficulty(nBits: number) {
|
|
const mantissa: number = nBits & 0x007fffff; // Extract the mantissa from nBits
|
|
const exponent: number = (nBits >> 24) & 0xff; // Extract the exponent from nBits
|
|
|
|
const target: number = mantissa * Math.pow(256, (exponent - 3)); // Calculate the target value
|
|
|
|
const difficulty: number = (Math.pow(2, 208) * 65535) / target; // Calculate the difficulty
|
|
|
|
return difficulty;
|
|
}
|
|
|
|
} |