From 3b47f41d6da681250f43e3fc5e50980f530b54bd Mon Sep 17 00:00:00 2001 From: keegreil Date: Sun, 13 Sep 2026 17:28:45 -0400 Subject: [PATCH] fix: expose complete native SV2 connection details --- boot.tests/NodeSetupTests.cs | 2 +- boot.tests/SecurityHardeningTests.cs | 21 ++++++++++++++ boot_portal/Models/DashboardModels.cs | 1 + boot_portal/Models/PoolConfig.cs | 3 ++ boot_portal/Pages/Setup.cshtml | 9 +++--- boot_portal/Pages/Setup.cshtml.cs | 2 ++ boot_portal/Program.cs | 7 +++++ .../Services/DashboardReadModelService.cs | 3 +- boot_portal/Utils/NodeSetupPolicy.cs | 1 - boot_portal/boot_portal_config.json | 1 + boot_portal/ui/AGENTS.md | 3 +- .../src/components/MinerConnection.test.tsx | 1 + .../ui/src/components/MinerConnection.tsx | 5 ++++ boot_portal/ui/src/test/fixture.ts | 1 + boot_portal/ui/src/types.ts | 1 + boot_portal/wwwroot/setup.css | 15 ++++++++++ boot_portal/wwwroot/setup.js | 29 ------------------- 17 files changed, 67 insertions(+), 38 deletions(-) delete mode 100644 boot_portal/wwwroot/setup.js diff --git a/boot.tests/NodeSetupTests.cs b/boot.tests/NodeSetupTests.cs index 5141989..e6a3df5 100644 --- a/boot.tests/NodeSetupTests.cs +++ b/boot.tests/NodeSetupTests.cs @@ -101,7 +101,7 @@ public void SetupModeAllowsOnlySetupAndHealthPaths() { Assert.IsTrue(NodeSetupPolicy.IsAllowedSetupPath("/setup")); Assert.IsTrue(NodeSetupPolicy.IsAllowedSetupPath("/setup.css")); - Assert.IsTrue(NodeSetupPolicy.IsAllowedSetupPath("/setup.js")); + Assert.IsFalse(NodeSetupPolicy.IsAllowedSetupPath("/setup.js")); Assert.IsTrue(NodeSetupPolicy.IsAllowedSetupPath("/health/live")); Assert.IsTrue(NodeSetupPolicy.IsAllowedSetupPath("/health/ready")); Assert.IsFalse(NodeSetupPolicy.IsAllowedSetupPath("/api/mining/share")); diff --git a/boot.tests/SecurityHardeningTests.cs b/boot.tests/SecurityHardeningTests.cs index dff7947..ca717fb 100644 --- a/boot.tests/SecurityHardeningTests.cs +++ b/boot.tests/SecurityHardeningTests.cs @@ -69,6 +69,27 @@ public void PrivateDashboardEnvironmentOverrideSupportsPackageUpgrades() } } + [TestMethod] + public void NativeSv2AuthorityEnvironmentOverrideSupportsPackageUpgrades() + { + const string variable = "GRIDPOOL_NATIVE_SV2_AUTHORITY_PUBLIC_KEY"; + const string publicKey = "9exampleAuthorityPublicKey"; + string? previous = Environment.GetEnvironmentVariable(variable); + try + { + var config = ValidConfig(); + Environment.SetEnvironmentVariable(variable, $" {publicKey} "); + + Program.ApplyPoolConfigEnvironmentOverrides(config); + + Assert.AreEqual(publicKey, config.NativeSv2AuthorityPublicKey); + } + finally + { + Environment.SetEnvironmentVariable(variable, previous); + } + } + [TestMethod] public void StoredMinerLabelIsBoundedAndMarkupFree() { diff --git a/boot_portal/Models/DashboardModels.cs b/boot_portal/Models/DashboardModels.cs index 174a618..79e8b91 100644 --- a/boot_portal/Models/DashboardModels.cs +++ b/boot_portal/Models/DashboardModels.cs @@ -40,6 +40,7 @@ public sealed class DashboardNativeSv2Dto public bool Enabled { get; set; } public string PublicHost { get; set; } = string.Empty; public int PublicPort { get; set; } = 34265; + public string AuthorityPublicKey { get; set; } = string.Empty; public string Scheme { get; set; } = "stratum2+noise"; public string UsernameGuidance { get; set; } = "Use a Bitcoin payout address for per-miner slot-0 attribution, or a worker label to use the node payout address."; diff --git a/boot_portal/Models/PoolConfig.cs b/boot_portal/Models/PoolConfig.cs index 11fdc41..9594ebd 100644 --- a/boot_portal/Models/PoolConfig.cs +++ b/boot_portal/Models/PoolConfig.cs @@ -328,6 +328,9 @@ public class PoolConfig [JsonPropertyName("native_sv2_public_port")] public int NativeSv2PublicPort { get; set; } = 34265; + [JsonPropertyName("native_sv2_authority_public_key")] + public string NativeSv2AuthorityPublicKey { get; set; } = string.Empty; + [JsonPropertyName("admin_rate_limit_per_minute")] public int AdminRateLimitPerMinute { get; set; } = 12; diff --git a/boot_portal/Pages/Setup.cshtml b/boot_portal/Pages/Setup.cshtml index 1d1886a..079068a 100644 --- a/boot_portal/Pages/Setup.cshtml +++ b/boot_portal/Pages/Setup.cshtml @@ -46,19 +46,22 @@
Host
@Model.NativeSv2Host
Port
@Model.NativeSv2Port
+
Authority key
@Model.NativeSv2AuthorityPublicKey
Username
Your payout address or worker label
+

AxeOS users must paste the authority key into SV2 Authority Pubkey under the pool's advanced options.

If @Model.NativeSv2Host does not resolve from your miner, use this Umbrel device's LAN IP with port @Model.NativeSv2Port.

} @if (Model.AutomaticRestart) { -

Waiting for the node to become ready. This page will open the dashboard automatically.

+

GridPool is restarting. Wait a few seconds, then open the dashboard.

} else {

GridPool will remain in setup-only mode until it restarts. No mining work or peer relay is active yet.

} + Open GridPool dashboard } else { @@ -77,9 +80,5 @@ } - @if (Model.AutomaticRestart) - { - - } diff --git a/boot_portal/Pages/Setup.cshtml.cs b/boot_portal/Pages/Setup.cshtml.cs index 3bfcdd0..484bba0 100644 --- a/boot_portal/Pages/Setup.cshtml.cs +++ b/boot_portal/Pages/Setup.cshtml.cs @@ -53,6 +53,8 @@ public string NativeSv2Host public int NativeSv2Port => _poolConfig.NativeSv2PublicPort; + public string NativeSv2AuthorityPublicKey => _poolConfig.NativeSv2AuthorityPublicKey; + public string NativeSv2Url => $"stratum2+noise://{NativeSv2Host}:{NativeSv2Port}"; public IActionResult OnGet() diff --git a/boot_portal/Program.cs b/boot_portal/Program.cs index 4f1b809..3100d2a 100644 --- a/boot_portal/Program.cs +++ b/boot_portal/Program.cs @@ -795,6 +795,13 @@ internal static void ApplyPoolConfigEnvironmentOverrides(PoolConfig config) { config.TrustedPrivateDashboardEnabled = enabled; } + + string? nativeSv2AuthorityPublicKey = + Environment.GetEnvironmentVariable("GRIDPOOL_NATIVE_SV2_AUTHORITY_PUBLIC_KEY"); + if (!string.IsNullOrWhiteSpace(nativeSv2AuthorityPublicKey)) + { + config.NativeSv2AuthorityPublicKey = nativeSv2AuthorityPublicKey.Trim(); + } } private static RateLimitPartition CreateRateLimitPartition(HttpContext context, PoolConfig poolConfig, string policyName, int permitLimit) diff --git a/boot_portal/Services/DashboardReadModelService.cs b/boot_portal/Services/DashboardReadModelService.cs index e0c7ff4..7745ce6 100644 --- a/boot_portal/Services/DashboardReadModelService.cs +++ b/boot_portal/Services/DashboardReadModelService.cs @@ -115,7 +115,8 @@ public DashboardSummaryDto BuildSummary(string? windowKey) { Enabled = _poolConfig.NativeSv2Enabled, PublicHost = _poolConfig.NativeSv2PublicHost, - PublicPort = _poolConfig.NativeSv2PublicPort + PublicPort = _poolConfig.NativeSv2PublicPort, + AuthorityPublicKey = _poolConfig.NativeSv2AuthorityPublicKey } }, Capabilities = new DashboardCapabilitiesDto diff --git a/boot_portal/Utils/NodeSetupPolicy.cs b/boot_portal/Utils/NodeSetupPolicy.cs index 1fb1653..6757355 100644 --- a/boot_portal/Utils/NodeSetupPolicy.cs +++ b/boot_portal/Utils/NodeSetupPolicy.cs @@ -6,7 +6,6 @@ public static bool IsAllowedSetupPath(PathString path) { return path.StartsWithSegments("/setup", StringComparison.OrdinalIgnoreCase) || path.Equals("/setup.css", StringComparison.OrdinalIgnoreCase) || - path.Equals("/setup.js", StringComparison.OrdinalIgnoreCase) || path.Equals("/health/live", StringComparison.OrdinalIgnoreCase) || path.Equals("/health/ready", StringComparison.OrdinalIgnoreCase); } diff --git a/boot_portal/boot_portal_config.json b/boot_portal/boot_portal_config.json index 825924f..6bc810e 100644 --- a/boot_portal/boot_portal_config.json +++ b/boot_portal/boot_portal_config.json @@ -60,6 +60,7 @@ "native_sv2_enabled": false, "native_sv2_public_host": "", "native_sv2_public_port": 34265, + "native_sv2_authority_public_key": "", "stratum_v1_proxy_host": "", "stratum_v1_proxy_port": 0, "grid_labs_support_fee_enabled": true, diff --git a/boot_portal/ui/AGENTS.md b/boot_portal/ui/AGENTS.md index b2b3a30..948a675 100644 --- a/boot_portal/ui/AGENTS.md +++ b/boot_portal/ui/AGENTS.md @@ -66,7 +66,8 @@ stay in React memory only and must not enter URLs, browser storage, logs, or exports. Miner-facing endpoints are explicit non-secret summary data. Packaged nodes set -`native_sv2_enabled`, `native_sv2_public_host`, and `native_sv2_public_port`; +`native_sv2_enabled`, `native_sv2_public_host`, `native_sv2_public_port`, and +`native_sv2_authority_public_key`; when the host is blank the dashboard suggests the browser hostname and explains that a reachable LAN hostname or IP may be substituted. Appliance wrappers may explicitly set `trusted_private_dashboard_enabled` when their authenticated diff --git a/boot_portal/ui/src/components/MinerConnection.test.tsx b/boot_portal/ui/src/components/MinerConnection.test.tsx index b320859..6232046 100644 --- a/boot_portal/ui/src/components/MinerConnection.test.tsx +++ b/boot_portal/ui/src/components/MinerConnection.test.tsx @@ -8,6 +8,7 @@ describe("MinerConnectionPanel", () => { render(); expect(screen.getByText("stratum2+noise://node.gridpool.test:34265")).toBeInTheDocument(); + expect(screen.getByText("9exampleAuthorityPublicKey")).toBeInTheDocument(); expect(screen.getByText("Your payout address, or a worker label")).toBeInTheDocument(); }); diff --git a/boot_portal/ui/src/components/MinerConnection.tsx b/boot_portal/ui/src/components/MinerConnection.tsx index e706bf0..3b451ef 100644 --- a/boot_portal/ui/src/components/MinerConnection.tsx +++ b/boot_portal/ui/src/components/MinerConnection.tsx @@ -5,6 +5,7 @@ const unavailableSv2 = { enabled: false, publicHost: "", publicPort: 34265, + authorityPublicKey: "", scheme: "stratum2+noise", usernameGuidance: "Use a Bitcoin payout address or worker label." }; @@ -49,10 +50,14 @@ export function MinerConnectionPanel({ summary }: { summary: DashboardSummary })
Host
{connectionHost(sv2.publicHost)}
Port
{sv2.publicPort}
+
Authority key
{sv2.authorityPublicKey || "Not advertised"}
Protocol
Native Stratum V2 with Noise
Username
Your payout address, or a worker label

{sv2.usernameGuidance}

+

+ AxeOS users must paste the authority key into SV2 Authority Pubkey under the pool's advanced options. +

The miner must be on a network that can reach this node. If the suggested hostname does not resolve from the miner, use the Umbrel device's LAN IP. diff --git a/boot_portal/ui/src/test/fixture.ts b/boot_portal/ui/src/test/fixture.ts index d905de4..a68c2e2 100644 --- a/boot_portal/ui/src/test/fixture.ts +++ b/boot_portal/ui/src/test/fixture.ts @@ -89,6 +89,7 @@ export const summaryFixture: DashboardSummary = { enabled: true, publicHost: "node.gridpool.test", publicPort: 34265, + authorityPublicKey: "9exampleAuthorityPublicKey", scheme: "stratum2+noise", usernameGuidance: "Use a payout address or worker label." } diff --git a/boot_portal/ui/src/types.ts b/boot_portal/ui/src/types.ts index 1112620..34f5eec 100644 --- a/boot_portal/ui/src/types.ts +++ b/boot_portal/ui/src/types.ts @@ -71,6 +71,7 @@ export interface DashboardSummary { enabled: boolean; publicHost: string; publicPort: number; + authorityPublicKey: string; scheme: string; usernameGuidance: string; }; diff --git a/boot_portal/wwwroot/setup.css b/boot_portal/wwwroot/setup.css index aec4f85..cd63636 100644 --- a/boot_portal/wwwroot/setup.css +++ b/boot_portal/wwwroot/setup.css @@ -166,6 +166,21 @@ button { button:hover { transform: translateY(-1px); } +.dashboard-button { + display: block; + margin-top: 1rem; + border: 1px solid var(--line-strong); + border-radius: 3px; + padding: 0.9rem 1.1rem; + background: var(--text); + color: var(--bg); + font-weight: 650; + text-align: center; + text-decoration: none; +} + +.dashboard-button:hover { transform: translateY(-1px); } + .field-validation, .validation-summary { display: block; margin-top: 0.6rem; color: var(--bad); } diff --git a/boot_portal/wwwroot/setup.js b/boot_portal/wwwroot/setup.js deleted file mode 100644 index 1ca98eb..0000000 --- a/boot_portal/wwwroot/setup.js +++ /dev/null @@ -1,29 +0,0 @@ -(() => { - const status = document.getElementById("restart-status"); - if (!status) return; - - const startedAt = Date.now(); - - async function waitForReady() { - try { - const response = await fetch("/api/dashboard/v1/summary?window=24h", { - cache: "no-store", - headers: { Accept: "application/json" } - }); - if (response.ok) { - window.location.replace("/"); - return; - } - } catch { - // The expected connection drop confirms that the container is restarting. - } - - if (Date.now() - startedAt > 120000) { - status.textContent = "GridPool is taking longer than expected to restart. You may leave this page open or restart the app from Umbrel."; - return; - } - window.setTimeout(waitForReady, 1000); - } - - window.setTimeout(waitForReady, 1000); -})();