mirror of
https://github.com/benjamin-wilson/public-pool.git
synced 2026-09-29 17:15:03 -07:00
Speed up template publication and share validation
This commit is contained in:
@@ -6,12 +6,12 @@ import { SV2_EXTENDED_TOTAL_EXTRANONCE_SIZE_BYTES } from '../stratum.constants';
|
||||
|
||||
export class Sv2ExtranonceManager {
|
||||
private nextPrefix: number;
|
||||
private readonly workerOffset: number;
|
||||
private readonly maxPrefix: number;
|
||||
private allocatedPrefixes = new Map<number, number>(); // channelId → prefix
|
||||
private usedPrefixes = new Set<number>();
|
||||
private readonly prefixSize: number;
|
||||
private readonly totalExtranonceSize: number;
|
||||
private readonly processNamespace: number;
|
||||
|
||||
/**
|
||||
* @param prefixSize Bytes used for pool-assigned prefix (default 4)
|
||||
@@ -19,13 +19,25 @@ export class Sv2ExtranonceManager {
|
||||
* SV2 extended jobs patch the coinbase script length when the
|
||||
* negotiated total differs from the SV1-compatible template slot.
|
||||
*/
|
||||
constructor(prefixSize = 4, totalExtranonceSize = SV2_EXTENDED_TOTAL_EXTRANONCE_SIZE_BYTES) {
|
||||
constructor(
|
||||
prefixSize = 4,
|
||||
totalExtranonceSize = SV2_EXTENDED_TOTAL_EXTRANONCE_SIZE_BYTES,
|
||||
processNamespace = 0,
|
||||
) {
|
||||
if (!Number.isInteger(prefixSize) || prefixSize < 2 || prefixSize > 4) {
|
||||
throw new Error('SV2 extranonce prefix size must be between 2 and 4 bytes');
|
||||
}
|
||||
if (!Number.isInteger(totalExtranonceSize) || totalExtranonceSize < prefixSize) {
|
||||
throw new Error('SV2 total extranonce size must include the pool prefix');
|
||||
}
|
||||
if (!Number.isInteger(processNamespace) || processNamespace < 0 || processNamespace > 0xff) {
|
||||
throw new Error('SV2 process namespace must fit in one byte');
|
||||
}
|
||||
this.prefixSize = prefixSize;
|
||||
this.totalExtranonceSize = totalExtranonceSize;
|
||||
this.processNamespace = processNamespace;
|
||||
|
||||
const workerId = 0;
|
||||
const bitsPerWorker = (prefixSize - 1) * 8;
|
||||
this.workerOffset = workerId * Math.pow(2, bitsPerWorker);
|
||||
this.maxPrefix = Math.pow(2, bitsPerWorker) - 1; // max within this worker's slice
|
||||
this.nextPrefix = 1;
|
||||
}
|
||||
@@ -43,6 +55,9 @@ export class Sv2ExtranonceManager {
|
||||
* Returns a Buffer of `prefixSize` bytes.
|
||||
*/
|
||||
allocate(channelId: number): Buffer {
|
||||
if (!Number.isInteger(channelId) || channelId < 1 || channelId > 0xffffffff) {
|
||||
throw new Error('SV2 channel ID must be an unsigned non-zero 32-bit integer');
|
||||
}
|
||||
// Check if already allocated
|
||||
if (this.allocatedPrefixes.has(channelId)) {
|
||||
const existing = this.allocatedPrefixes.get(channelId)!;
|
||||
@@ -52,19 +67,17 @@ export class Sv2ExtranonceManager {
|
||||
// Find next available prefix within this worker's partition
|
||||
let localPrefix = this.nextPrefix;
|
||||
let attempts = 0;
|
||||
const globalPrefix = () => this.workerOffset + localPrefix;
|
||||
|
||||
while (this.usedPrefixes.has(globalPrefix()) && attempts <= this.maxPrefix) {
|
||||
while (this.usedPrefixes.has(localPrefix) && attempts <= this.maxPrefix) {
|
||||
localPrefix = localPrefix + 1;
|
||||
if (localPrefix > this.maxPrefix) localPrefix = 1; // Wrap within partition, skip 0
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (this.usedPrefixes.has(globalPrefix())) {
|
||||
if (this.usedPrefixes.has(localPrefix)) {
|
||||
throw new Error('Extranonce prefix space exhausted');
|
||||
}
|
||||
|
||||
const prefix = globalPrefix();
|
||||
const prefix = localPrefix;
|
||||
this.allocatedPrefixes.set(channelId, prefix);
|
||||
this.usedPrefixes.add(prefix);
|
||||
this.nextPrefix = localPrefix + 1;
|
||||
@@ -99,19 +112,90 @@ export class Sv2ExtranonceManager {
|
||||
|
||||
private prefixToBuffer(prefix: number): Buffer {
|
||||
const buf = Buffer.alloc(this.prefixSize);
|
||||
// Write as big-endian so prefix 1 = 0x00000001
|
||||
if (this.prefixSize === 4) {
|
||||
buf.writeUInt32BE(prefix, 0);
|
||||
} else if (this.prefixSize === 2) {
|
||||
buf.writeUInt16BE(prefix, 0);
|
||||
} else {
|
||||
// Generic: write big-endian
|
||||
let val = prefix;
|
||||
for (let i = this.prefixSize - 1; i >= 0; i--) {
|
||||
buf[i] = val & 0xff;
|
||||
val = val >>> 8;
|
||||
}
|
||||
buf[0] = this.processNamespace;
|
||||
// The remaining bytes are a process-local, big-endian allocation. Prefix
|
||||
// zero stays reserved so an all-zero pool prefix is never issued.
|
||||
let val = prefix;
|
||||
for (let i = this.prefixSize - 1; i >= 1; i--) {
|
||||
buf[i] = val % 0x100;
|
||||
val = Math.floor(val / 0x100);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
||||
export const SV2_MAX_PROCESS_NAMESPACE = 0xff;
|
||||
|
||||
/**
|
||||
* Resolve the byte that namespaces every pool-assigned SV2 extranonce prefix.
|
||||
* PM2 can overlap old and new workers during reload, so each worker receives
|
||||
* two lanes selected by restart-generation parity.
|
||||
*/
|
||||
export function resolveSv2ProcessNamespace(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
clusterWorkerIndex?: number,
|
||||
): number {
|
||||
const base = parseNamespaceInteger(
|
||||
'SV2_EXTRANONCE_NAMESPACE_BASE',
|
||||
env.SV2_EXTRANONCE_NAMESPACE_BASE,
|
||||
true,
|
||||
) ?? 0;
|
||||
const nodeAppInstance = parseNamespaceInteger(
|
||||
'NODE_APP_INSTANCE',
|
||||
env.NODE_APP_INSTANCE,
|
||||
true,
|
||||
);
|
||||
const pmId = parseNamespaceInteger('pm_id', env.pm_id, true);
|
||||
const workerIndex = nodeAppInstance ?? pmId ?? clusterWorkerIndex;
|
||||
|
||||
if (workerIndex == null) {
|
||||
if (env.PM2_ENABLED?.toLowerCase() === 'true') {
|
||||
throw new Error(
|
||||
'PM2 SV2 worker has no NODE_APP_INSTANCE or pm_id; cannot allocate collision-free extranonces',
|
||||
);
|
||||
}
|
||||
assertNamespaceFits(base);
|
||||
return base;
|
||||
}
|
||||
if (!Number.isInteger(workerIndex) || workerIndex < 0) {
|
||||
throw new Error('SV2 worker index must be a non-negative integer');
|
||||
}
|
||||
|
||||
const restartGeneration = parseNamespaceInteger(
|
||||
'restart_time',
|
||||
env.restart_time,
|
||||
true,
|
||||
) ?? 0;
|
||||
const namespace = base + (workerIndex * 2) + (restartGeneration % 2);
|
||||
assertNamespaceFits(namespace);
|
||||
return namespace;
|
||||
}
|
||||
|
||||
function parseNamespaceInteger(
|
||||
name: string,
|
||||
value: string | undefined,
|
||||
optional: boolean,
|
||||
): number | null {
|
||||
if (value == null || value.trim().length === 0) {
|
||||
if (optional) {
|
||||
return null;
|
||||
}
|
||||
throw new Error(`${name} is required`);
|
||||
}
|
||||
if (!/^\d+$/.test(value.trim())) {
|
||||
throw new Error(`${name} must be a non-negative integer`);
|
||||
}
|
||||
const parsed = Number(value);
|
||||
if (!Number.isSafeInteger(parsed)) {
|
||||
throw new Error(`${name} must be a safe integer`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function assertNamespaceFits(namespace: number): void {
|
||||
if (!Number.isInteger(namespace) || namespace < 0 || namespace > SV2_MAX_PROCESS_NAMESPACE) {
|
||||
throw new Error(
|
||||
`SV2 extranonce namespace ${namespace} does not fit in one byte; reduce worker count or namespace base`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user