Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 91 additions & 5 deletions server/src/config-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { test } from 'node:test';
import { generateJdcConfig, generateTranslatorConfig, normalizeSetupData } from './config-generator.js';
import type { SetupData } from './types.js';

const BASE_DATA: SetupData = {
const BASE_DATA_30_2: SetupData = {
miningMode: 'pool',
mode: 'jd',
pool: {
Expand All @@ -13,6 +13,7 @@ const BASE_DATA: SetupData = {
authority_public_key: 'authority-key',
},
bitcoin: {
core_version: '30.2',
network: 'testnet4',
os: 'linux',
customDataDir: '',
Expand All @@ -33,15 +34,47 @@ const BASE_DATA: SetupData = {
},
};

const BASE_DATA_31_0: SetupData = {
...BASE_DATA_30_2,
bitcoin: { ...BASE_DATA_30_2.bitcoin!, core_version: '31.0' },
};

const BASE_DATA_31_0_SOLO: SetupData = {
...BASE_DATA_31_0,
miningMode: 'solo',
pool: null,
};

const NO_JD_DATA: SetupData = {
miningMode: 'pool',
mode: 'no-jd',
pool: {
name: 'Remote Pool',
address: 'remote.pool.com',
port: 3333,
authority_public_key: 'remote-pool-key',
},
bitcoin: null,
jdc: null,
translator: {
user_identity: 'miner.solo',
enable_vardiff: true,
aggregate_channels: false,
min_hashrate: 100_000_000_000_000,
shares_per_minute: 6,
downstream_extranonce2_size: 4,
},
};

test('translator config uses advanced setup values', () => {
const config = generateTranslatorConfig(BASE_DATA);
const config = generateTranslatorConfig(BASE_DATA_30_2);

assert.match(config, /downstream_extranonce2_size = 8/);
assert.match(config, /shares_per_minute = 12\.5/);
});

test('jdc config uses shared shares-per-minute and miner signature', () => {
const config = generateJdcConfig(BASE_DATA);
const config = generateJdcConfig(BASE_DATA_30_2);

assert.ok(config);
assert.match(config, /shares_per_minute = 12\.5/);
Expand All @@ -50,16 +83,69 @@ test('jdc config uses shared shares-per-minute and miner signature', () => {

test('normalization backfills advanced defaults for old saved configs', () => {
const data = {
...BASE_DATA,
...BASE_DATA_30_2,
translator: {
...BASE_DATA.translator,
...BASE_DATA_30_2.translator,
shares_per_minute: undefined,
downstream_extranonce2_size: undefined,
},
} as unknown as SetupData;

const normalized = normalizeSetupData(data);

assert.ok(normalized.translator);
assert.equal(normalized.translator.shares_per_minute, 6);
assert.equal(normalized.translator.downstream_extranonce2_size, 4);
});

test('old format (v0.3.5): translator puts user_identity at top level, not inside [[upstreams]]', () => {
const config = generateTranslatorConfig(BASE_DATA_30_2);

assert.match(config, /^user_identity = "miner\.worker1"/m);
assert.doesNotMatch(config, /\[\[upstreams\]\][\s\S]*user_identity = "miner\.worker1"/);
});

test('old format (v0.3.5): jdc puts user_identity at top level, not inside [[upstreams]]', () => {
const config = generateJdcConfig(BASE_DATA_30_2);

assert.ok(config);
assert.match(config, /^user_identity = "miner\.worker1"/m);
assert.doesNotMatch(config, /\[\[upstreams\]\][\s\S]*user_identity = "miner\.worker1"/);
});

test('new format (main): translator puts user_identity inside [[upstreams]], not at top level', () => {
const config = generateTranslatorConfig(BASE_DATA_31_0);
const upstreamIdx = config.indexOf('[[upstreams]]');
const identityIdx = config.indexOf('user_identity');

assert.ok(identityIdx > upstreamIdx);
assert.match(config, /\[\[upstreams\]\][\s\S]*user_identity = "miner\.worker1"/);
});

test('new format (main): jdc in pool mode puts user_identity inside [[upstreams]], not at top level', () => {
const config = generateJdcConfig(BASE_DATA_31_0);

assert.ok(config);
const upstreamIdx = config.indexOf('[[upstreams]]');
const identityIdx = config.indexOf('user_identity');

assert.ok(identityIdx > upstreamIdx);
assert.match(config, /\[\[upstreams\]\][\s\S]*user_identity = "miner\.worker1"/);
});

test('new format (main): jdc in solo mode omits user_identity entirely', () => {
const config = generateJdcConfig(BASE_DATA_31_0_SOLO);

assert.ok(config);
assert.doesNotMatch(config, /user_identity/);
assert.match(config, /upstreams = \[\]/);
});

test('no-jd mode: translator uses new format (user_identity inside [[upstreams]])', () => {
const config = generateTranslatorConfig(NO_JD_DATA);
const upstreamIdx = config.indexOf('[[upstreams]]');
const identityIdx = config.indexOf('user_identity');

assert.ok(identityIdx > upstreamIdx);
assert.match(config, /\[\[upstreams\]\][\s\S]*user_identity = "miner\.solo"/);
});
31 changes: 23 additions & 8 deletions server/src/config-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ function positiveInteger(value: number | undefined, fallback: number): number {
return Math.max(1, Math.trunc(normalized));
}

function isNewUpstreamFormat(data: SetupData): boolean {
// no-jd mode always uses :main images, new format (user_identity inside [[upstreams]])
if (data.mode !== 'jd') return true;
// jd mode: 31.0+ uses :main images, new format; 30.2 uses :v0.3.5 → old (top-level user_identity)
return data.bitcoin?.core_version !== '30.2';
}

export function normalizeSetupData(data: SetupData): SetupData {
if (!data.translator) {
return data;
Expand All @@ -50,11 +57,11 @@ export function generateTranslatorConfig(data: SetupData): string {
const normalizedData = normalizeSetupData(data);
const { pool, translator, mode } = normalizedData;
const isJdMode = mode === 'jd';

if (!translator || (!isJdMode && !pool)) {
throw new Error('Pool and translator configuration are required');
}

// If JD mode, translator connects to JDC container; otherwise directly to pool
// Both containers are on sv2-network, so we can use the container name as hostname
// (hostname resolution supported since sv2-apps PR #286)
Expand All @@ -66,6 +73,8 @@ export function generateTranslatorConfig(data: SetupData): string {
? JDC_AUTHORITY_PUBLIC_KEY
: pool!.authority_public_key;

const useNewFormat = isNewUpstreamFormat(normalizedData);

// Min hashrate from user config (default 100 TH/s if not set)
const minHashrate = translator.min_hashrate ? `${translator.min_hashrate}.0` : '100_000_000_000_000.0';
// Shares per minute target
Expand All @@ -75,6 +84,8 @@ export function generateTranslatorConfig(data: SetupData): string {
DEFAULT_DOWNSTREAM_EXTRANONCE2_SIZE,
);

const userIdentityLine = `user_identity = "${translator.user_identity}"`;

return `# Translator Proxy Configuration
# Generated by sv2-ui

Expand All @@ -89,10 +100,10 @@ min_supported_version = 2
# Extranonce2 size for downstream connections
downstream_extranonce2_size = ${downstreamExtranonce2Size}

# User identity/username for the upstream connection
user_identity = "${translator.user_identity}"
${useNewFormat ? '' : `# User identity/username for the upstream connection
${userIdentityLine}

# Aggregate channels: if true, all miners share one upstream channel
`}# Aggregate channels: if true, all miners share one upstream channel
aggregate_channels = ${translator.aggregate_channels}

# Protocol extensions configuration
Expand All @@ -114,6 +125,7 @@ job_keepalive_interval_secs = 60
address = "${upstreamAddress}"
port = ${upstreamPort}
authority_pubkey = "${authorityPubkey}"
${useNewFormat ? userIdentityLine : ''}
`;
}

Expand All @@ -126,6 +138,7 @@ export function generateJdcConfig(data: SetupData): string | null {
}

const { pool, jdc, bitcoin } = data;
const useNewFormat = isNewUpstreamFormat(data);
const isSovereignSolo = data.miningMode === 'solo';
const jdcSignature = isSovereignSolo ? (jdc.jdc_signature || jdc.user_identity) : jdc.jdc_signature;

Expand All @@ -138,6 +151,7 @@ export function generateJdcConfig(data: SetupData): string | null {
// Fee threshold and min interval for template provider
const feeThreshold = '1000';
const minInterval = '5';
const userIdentityLine = `user_identity = "${jdc.user_identity}"`;
const upstreamsConfig = !isSovereignSolo && pool
? `# Upstream pool connection
[[upstreams]]
Expand All @@ -146,6 +160,7 @@ pool_address = "${pool.address}"
pool_port = ${pool.port}
jds_address = "${pool.address}"
jds_port = 3334
${useNewFormat ? userIdentityLine : ''}

`
: `# No upstreams needed in solo mining mode.
Expand All @@ -167,10 +182,10 @@ authority_public_key = "${JDC_AUTHORITY_PUBLIC_KEY}"
authority_secret_key = "mkDLTBBRxdBv998612qipDYoTK3YUrqLe8uWw7gu3iXbSrn2n"
cert_validity_sec = 3600

# User identity/username for the upstream connection
user_identity = "${jdc.user_identity}"
${useNewFormat ? '' : `# User identity/username for the upstream connection
${userIdentityLine}

# Shares configuration
`}# Shares configuration
shares_per_minute = ${sharesPerMinute}
share_batch_size = ${shareBatchSize}

Expand Down
Loading