From 493a175839aff4fef95ee2ad238d73e2cc45ccd3 Mon Sep 17 00:00:00 2001 From: Lucas Balieiro <37416577+lucasbalieiro@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:25:58 -0300 Subject: [PATCH] add branching logic to the config generator to account for older sv2-apps version --- server/src/config-generator.test.ts | 96 +++++++++++++++++++++++++++-- server/src/config-generator.ts | 31 +++++++--- 2 files changed, 114 insertions(+), 13 deletions(-) diff --git a/server/src/config-generator.test.ts b/server/src/config-generator.test.ts index 6c1e8e2..a6961df 100644 --- a/server/src/config-generator.test.ts +++ b/server/src/config-generator.test.ts @@ -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: { @@ -13,6 +13,7 @@ const BASE_DATA: SetupData = { authority_public_key: 'authority-key', }, bitcoin: { + core_version: '30.2', network: 'testnet4', os: 'linux', customDataDir: '', @@ -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/); @@ -50,9 +83,9 @@ 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, }, @@ -60,6 +93,59 @@ test('normalization backfills advanced defaults for old saved configs', () => { 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"/); +}); diff --git a/server/src/config-generator.ts b/server/src/config-generator.ts index e0dc361..2e90429 100644 --- a/server/src/config-generator.ts +++ b/server/src/config-generator.ts @@ -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; @@ -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) @@ -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 @@ -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 @@ -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 @@ -114,6 +125,7 @@ job_keepalive_interval_secs = 60 address = "${upstreamAddress}" port = ${upstreamPort} authority_pubkey = "${authorityPubkey}" +${useNewFormat ? userIdentityLine : ''} `; } @@ -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; @@ -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]] @@ -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. @@ -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}