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
33 changes: 31 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,26 +43,53 @@ jobs:
dotnet-version: 10.0.x

- name: Restore app
run: dotnet restore boot_portal/boot_portal.csproj
run: dotnet restore boot_portal/boot_portal.csproj --locked-mode

- name: Build app
run: dotnet build boot_portal/boot_portal.csproj --configuration Release --no-restore

- name: Restore tests
run: dotnet restore boot.tests/boot.tests.csproj
run: dotnet restore boot.tests/boot.tests.csproj --locked-mode

- name: Build tests
run: dotnet build boot.tests/boot.tests.csproj --configuration Release --no-restore

- name: Run tests
run: dotnet test boot.tests/boot.tests.csproj --configuration Release --no-build

- name: Dependency advisory report
run: dotnet list boot_portal/boot_portal.csproj package --vulnerable --include-transitive

- name: Filesystem vulnerability scan
uses: aquasecurity/trivy-action@v0.36.0
with:
scan-type: fs
scan-ref: .
severity: CRITICAL,HIGH
ignore-unfixed: true
exit-code: '1'

secrets:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Scan repository history for secrets
run: >-
docker run --rm -v "$PWD:/repo"
ghcr.io/gitleaks/gitleaks@sha256:c00b6bd0aeb3071cbcb79009cb16a60dd9e0a7c60e2be9ab65d25e6bc8abbb7f
detect --source=/repo --redact --no-banner

docker:
runs-on: ubuntu-latest
needs: dotnet
permissions:
contents: read
packages: write
id-token: write
attestations: write

steps:
- name: Checkout
Expand Down Expand Up @@ -105,5 +132,7 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GRIDPOOL_RELEASE_VERSION=${{ github.ref_name }}+${{ github.sha }}
sbom: true
provenance: mode=max
cache-from: type=gha
cache-to: type=gha,mode=max
9 changes: 9 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Audited historical fixtures and retired prototype identities.
7ded520203d0c6859e1de70892651e12be8605c0:scripts/main-beta-status.sh:curl-auth-user:36
428a84391ec4bc119cb32391ce933cfd318ca8e4:boot.tests/PoolConfigValidatorTests.cs:generic-api-key:142
a636ac92f856e35fff3c7438d5d3acbfdf24b7d4:boot_portal/boot_portal_config.json:generic-api-key:3
07f8da37b297264cbc349ecc695c705e59392f6a:boot_portal/boot_portal_config.json:generic-api-key:2
07f8da37b297264cbc349ecc695c705e59392f6a:boot_portal/boot_portal_config.json:generic-api-key:3
2df39a9959680f42455d178d661912af37a627ac:boot_portal/boot_portal_config.json:generic-api-key:3
f8d788e4b771aea87f39a9b6ba9b19e86eb90dd3:boot_portal/boot_portal_config.json:generic-api-key:3
f8d788e4b771aea87f39a9b6ba9b19e86eb90dd3:boot_portal/boot_portal_config.json:generic-api-key:2
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
ARG GRIDPOOL_RELEASE_VERSION=dev

FROM node:24-bookworm-slim AS dashboard-build
FROM node:24-bookworm-slim@sha256:ba849c60be29959425b8734d57b8b4b7d56f98edd9504c9af091d5281095a71e AS dashboard-build
WORKDIR /src/boot_portal/ui
COPY boot_portal/ui/package.json boot_portal/ui/package-lock.json ./
RUN npm ci
COPY boot_portal/ui/ ./
RUN npm run build

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
FROM mcr.microsoft.com/dotnet/sdk:10.0@sha256:4ea6fe75dd36706bb6d8c3c293d4c4315840f5d76ea28ac97def77e3ec487fa5 AS build
WORKDIR /src

COPY boot_portal/boot_portal.csproj boot_portal/
RUN dotnet restore boot_portal/boot_portal.csproj
RUN dotnet restore boot_portal/boot_portal.csproj --locked-mode

COPY . .
COPY --from=dashboard-build /src/boot_portal/wwwroot/dashboard boot_portal/wwwroot/dashboard
RUN dotnet publish boot_portal/boot_portal.csproj -c Release -o /app/publish /p:UseAppHost=false

FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble AS runtime
FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble@sha256:1fe86375600b62e6566b465da9553eef0621f13c67f40fe764cd8dbb1dee1497 AS runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends libsodium23 ca-certificates curl \
&& rm -rf /var/lib/apt/lists/* \
Expand Down
35 changes: 35 additions & 0 deletions boot.tests/BitcoinHashesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using boot_portal.Utils;

namespace boot.tests;

[TestClass]
public sealed class BitcoinHashesTests
{
private const string RegtestGenesisHeader =
"01000000" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"3ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a" +
"dae5494d" +
"ffff7f20" +
"02000000";

[TestMethod]
public void RegtestPowLimitAcceptsRegtestGenesisAndMainnetRejectsIt()
{
BitcoinHeaderEvaluation regtest = BitcoinHashes.EvaluateHeader(
RegtestGenesisHeader,
DateTime.UtcNow,
BitcoinScript.Regtest);
BitcoinHeaderEvaluation mainnet = BitcoinHashes.EvaluateHeader(
RegtestGenesisHeader,
DateTime.UtcNow,
BitcoinScript.Mainnet);

Assert.IsTrue(regtest.IsValid, regtest.RejectionReason);
Assert.AreEqual(
"0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206",
regtest.BlockHash);
Assert.IsFalse(mainnet.IsValid);
StringAssert.Contains(mainnet.RejectionReason, "proof-of-work limit");
}
}
3 changes: 2 additions & 1 deletion boot.tests/BitcoinRpcClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public async Task ClientParsesBlockchainAndZmqResponsesWithoutEmbeddingCredentia
string result = method switch
{
"getblockchaininfo" => """
{"blocks":100,"headers":100,"bestblockhash":"block-100","initialblockdownload":false,"verificationprogress":1.0}
{"chain":"regtest","blocks":100,"headers":100,"bestblockhash":"block-100","initialblockdownload":false,"verificationprogress":1.0}
""",
"getzmqnotifications" => """
[{"type":"pubhashblock","address":"tcp://0.0.0.0:28332"},{"type":"pubrawblock","address":"tcp://0.0.0.0:28333"}]
Expand All @@ -46,6 +46,7 @@ public async Task ClientParsesBlockchainAndZmqResponsesWithoutEmbeddingCredentia

Assert.AreEqual(100L, info.Blocks);
Assert.AreEqual("block-100", info.BestBlockHash);
Assert.AreEqual("regtest", info.Chain);
CollectionAssert.AreEquivalent(
new[] { "pubhashblock", "pubrawblock" },
topics.Select(topic => topic.Topic).ToArray());
Expand Down
6 changes: 3 additions & 3 deletions boot.tests/BitcoinRpcRecoveryPlannerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,20 @@ public void SameHeightReplacementUsesReorganizationPath()
100,
"new-block-100");

CollectionAssert.AreEqual(new long[] { 100 }, plan.Heights.ToArray());
CollectionAssert.AreEqual(new long[] { 99, 100 }, plan.Heights.ToArray());
Assert.IsTrue(plan.Reorganization);
}

[TestMethod]
public void LowerRpcHeightPausesUntilReplacementChainCatchesUp()
public void LowerRpcHeightReplaysReplacementTipAndParent()
{
BitcoinRpcRecoveryPlan plan = BitcoinRpcRecoveryPlanner.Build(
101,
"disconnected-block-101",
100,
"replacement-block-100");

Assert.AreEqual(0, plan.Heights.Count);
CollectionAssert.AreEqual(new long[] { 99, 100 }, plan.Heights.ToArray());
Assert.IsTrue(plan.Reorganization);
}

Expand Down
20 changes: 19 additions & 1 deletion boot.tests/BitcoinZmqNotificationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public void AttachedNodeRequiresSynchronizedRpcAndRedactsErrors()
BitcoinRpcUrl = "http://bitcoin:8332",
BitcoinRpcLagGraceSeconds = 1
});
DateTime nowUtc = DateTime.UtcNow;
DateTime nowUtc = DateTime.UtcNow.AddSeconds(2);
health.RecordRpcFailure(
"failed http://alice:secret@bitcoin:8332/wallet/private?token=secret",
nowUtc.AddSeconds(-2));
Expand Down Expand Up @@ -188,4 +188,22 @@ public void BitcoinPeerTelemetryUsesStableProcessLocalVisualIds()
Assert.AreNotEqual(7L, first);
Assert.AreEqual(first, second);
}

[TestMethod]
public void RepeatedRpcFailuresCannotRenewMiningSafetyGracePeriod()
{
var health = new BitcoinNotificationHealth(new PoolConfig
{
BitcoinNotificationMode = BitcoinNotificationModes.AttachedNode,
BitcoinRpcUrl = "http://bitcoin:8332",
BitcoinRpcLagGraceSeconds = 1
});
DateTime afterGrace = DateTime.UtcNow.AddSeconds(2);

health.RecordRpcFailure("warmup", afterGrace.AddMilliseconds(-500));
health.RecordRpcFailure("still warming up", afterGrace);

Assert.IsFalse(health.IsMiningSafe(afterGrace, out string reason));
StringAssert.Contains(reason, "unreachable");
}
}
80 changes: 73 additions & 7 deletions boot.tests/PoolConfigValidatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@ namespace boot.tests;
[TestClass]
public sealed class PoolConfigValidatorTests
{
private const string MainnetPayoutAddress = "bc1qd9m04z95mglaxd9e9accmhyjdlmkfmzjprkq4p";

[TestMethod]
public void DefaultCoinbaseTagIsGridPool()
public void SetupUiMayStartWithoutPayoutButHeadlessModeCannot()
{
var config = new PoolConfig();

Assert.AreEqual("Grid Pool", config.CoinbaseTag);
CollectionAssert.AreEqual(Array.Empty<string>(), PoolConfigValidator.Validate(config));
Assert.IsFalse(PoolConfigValidator.Validate(config).Any(error =>
error.Contains("pool_payout_script", StringComparison.OrdinalIgnoreCase)));

config.EnableWebUi = false;
Assert.IsTrue(PoolConfigValidator.Validate(config).Any(error =>
error.Contains("pool_payout_script", StringComparison.OrdinalIgnoreCase)));
}

[TestMethod]
Expand Down Expand Up @@ -130,7 +137,9 @@ public void EmptyCoinbaseTagIsAllowed()
{
var config = new PoolConfig
{
CoinbaseTag = string.Empty
CoinbaseTag = string.Empty,
PoolPayoutScript = MainnetPayoutAddress,
EnableAdminApi = false
};

CollectionAssert.AreEqual(Array.Empty<string>(), PoolConfigValidator.Validate(config));
Expand Down Expand Up @@ -207,6 +216,7 @@ public void Testnet4AcceptsTestnetPayoutAddressAndRejectsMainnetAddress()
{
BitcoinNetwork = BitcoinScript.Testnet4,
PoolPayoutScript = testnetAddress,
EnableAdminApi = false
};

CollectionAssert.AreEqual(Array.Empty<string>(), PoolConfigValidator.Validate(validConfig));
Expand All @@ -222,14 +232,66 @@ public void UnsupportedBitcoinNetworkFailsValidation()
{
var config = new PoolConfig
{
BitcoinNetwork = "regtest"
BitcoinNetwork = "signet"
};

List<string> errors = PoolConfigValidator.Validate(config);

Assert.IsTrue(errors.Any(error => error.Contains("bitcoin_network", StringComparison.OrdinalIgnoreCase)));
}

[TestMethod]
public void RegtestUsesBcrtAddressesAndRejectsMainnetAddresses()
{
byte[] script = Enumerable.Range(0, 22).Select(i => (byte)i).ToArray();
script[0] = 0x00;
script[1] = 0x14;
string regtestAddress = BitcoinScript.ScriptToAddress(script, BitcoinScript.Regtest);
var config = new PoolConfig
{
BitcoinNetwork = BitcoinScript.Regtest,
PoolPayoutScript = regtestAddress,
EnableAdminApi = false
};

Assert.IsTrue(regtestAddress.StartsWith("bcrt1", StringComparison.OrdinalIgnoreCase));
CollectionAssert.AreEqual(Array.Empty<string>(), PoolConfigValidator.Validate(config));

config.PoolPayoutScript = "bc1qrwsx8fs0l6z7ugp5cvzy6lhss7jlyru3kg9s8y";
Assert.IsTrue(PoolConfigValidator.Validate(config).Any(error =>
error.Contains("pool_payout_script", StringComparison.OrdinalIgnoreCase)));
}

[TestMethod]
public void EmptySnapshotBootstrapIsRestrictedToNonProductionRegtest()
{
var regtest = new PoolConfig
{
BitcoinNetwork = BitcoinScript.Regtest,
NodeMode = "development",
AllowEmptySnapshotBootstrap = true
};
var mainnet = new PoolConfig
{
BitcoinNetwork = BitcoinScript.Mainnet,
NodeMode = "development",
AllowEmptySnapshotBootstrap = true
};
var productionRegtest = new PoolConfig
{
BitcoinNetwork = BitcoinScript.Regtest,
NodeMode = "production",
AllowEmptySnapshotBootstrap = true
};

Assert.IsFalse(PoolConfigValidator.Validate(regtest).Any(error =>
error.Contains("allow_empty_snapshot_bootstrap", StringComparison.OrdinalIgnoreCase)));
Assert.IsTrue(PoolConfigValidator.Validate(mainnet).Any(error =>
error.Contains("allow_empty_snapshot_bootstrap", StringComparison.OrdinalIgnoreCase)));
Assert.IsTrue(PoolConfigValidator.Validate(productionRegtest).Any(error =>
error.Contains("allow_empty_snapshot_bootstrap", StringComparison.OrdinalIgnoreCase)));
}

[TestMethod]
public void BadRateLimitFailsValidation()
{
Expand All @@ -250,7 +312,9 @@ public void SovereignModeIsAcceptedForInstallerNodes()
{
NodeMode = "sovereign",
PublicBaseUrl = "http://edge-node.local:5000",
DatumPublicHost = "edge-node.local"
DatumPublicHost = "edge-node.local",
PoolPayoutScript = MainnetPayoutAddress,
EnableAdminApi = false
};

CollectionAssert.AreEqual(Array.Empty<string>(), PoolConfigValidator.Validate(config));
Expand Down Expand Up @@ -301,7 +365,8 @@ public void ProductionAcceptsExplicitPublicEndpoints()
PublicBaseUrl = "https://use1.gridlabs.science",
DatumPublicHost = "datum-use1.gridlabs.science",
EnableAdminApi = false,
TestingRoundResetMode = "none"
TestingRoundResetMode = "none",
PoolPayoutScript = MainnetPayoutAddress
};

CollectionAssert.AreEqual(Array.Empty<string>(), PoolConfigValidator.Validate(config));
Expand Down Expand Up @@ -335,7 +400,8 @@ public void ProductionAcceptsStrongAdminKeyWhenAdminApiIsEnabled()
DatumPublicHost = "datum-use1.gridlabs.science",
EnableAdminApi = true,
AdminApiKey = new string('a', 32),
TestingRoundResetMode = "none"
TestingRoundResetMode = "none",
PoolPayoutScript = MainnetPayoutAddress
};

CollectionAssert.AreEqual(Array.Empty<string>(), PoolConfigValidator.Validate(config));
Expand Down
Loading
Loading