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
24 changes: 14 additions & 10 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,25 @@

jobs:
docker:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-24.04
- arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout
uses: actions/checkout@v7
# Set up Docker Buildx for multi-arch support
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
# Build multi-arch image
- name: Build Docker image (multi-arch)
run: |
docker buildx build --platform linux/amd64,linux/arm64 .
# Docker development
- name: Docker development
run: docker buildx build -f Dockerfile.brain.dev --platform linux/amd64,linux/arm64 .
- name: Build production image
run: docker buildx build --platform linux/${{ matrix.arch }} .
- name: Build development image
run: docker buildx build -f Dockerfile.brain.dev --platform linux/${{ matrix.arch }} .
build:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand All @@ -33,7 +37,7 @@
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: 24
node-version: 24.22.1

- name: Enable Corepack
run: corepack enable
Expand Down
60 changes: 52 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,23 @@ on:

jobs:
push-to-ghcr:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-24.04
- arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v7
with:
ref: ${{ inputs.tag }}

- name: Login to GHCR
uses: docker/login-action@v4
Expand All @@ -24,30 +34,64 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

# Set up Docker Buildx for multi-arch support
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

# Build and push multi-arch Docker image
- name: Build and push Staking brain docker monorepo
- name: Build and push native image
uses: docker/build-push-action@v7
with:
context: .
file: Dockerfile
push: true
platforms: linux/amd64, linux/arm64
tags: ghcr.io/${{ github.repository_owner }}/staking-brain:${{ github.event.inputs.tag }}
platforms: linux/${{ matrix.arch }}
tags: ghcr.io/${{ github.repository_owner }}/staking-brain:build-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.arch }}

publish-manifest:
runs-on: ubuntu-24.04
needs: push-to-ghcr
permissions:
contents: read
packages: write
steps:
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Publish multi-platform image
env:
IMAGE: ghcr.io/${{ github.repository_owner }}/staking-brain
VERSION: ${{ inputs.tag }}
BUILD: build-${{ github.run_id }}-${{ github.run_attempt }}
run: |
docker buildx imagetools create \
--tag "$IMAGE:$VERSION" \
"$IMAGE:$BUILD-amd64" \
"$IMAGE:$BUILD-arm64"

gh-release:
runs-on: ubuntu-latest
needs: push-to-ghcr
needs: publish-manifest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v7
with:
ref: ${{ inputs.tag }}

- name: Create Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ inputs.tag }}
run: gh release create "$RELEASE_TAG" --title "Release v$RELEASE_TAG" --prerelease --generate-notes
run: |
case "$RELEASE_TAG" in
*-*) release_flag=--prerelease ;;
*) release_flag=--latest ;;
esac
gh release create "$RELEASE_TAG" --verify-tag --title "Release v$RELEASE_TAG" "$release_flag" --generate-notes
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24.22.1
6 changes: 5 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
ARG DOCKER_IMAGE=node:24.21.0-alpine
ARG DOCKER_IMAGE=node:24.22.1-alpine3.24

# Build
FROM ${DOCKER_IMAGE} AS build-stage

RUN apk add --no-cache --upgrade ca-certificates ca-certificates-bundle && update-ca-certificates

WORKDIR /app
COPY package.json yarn.lock tsconfig.json .yarnrc.yml ./
COPY packages packages
Expand All @@ -19,6 +21,8 @@ RUN yarn install --immutable && \
# Production
FROM ${DOCKER_IMAGE}
ENV NODE_ENV=production
RUN apk add --no-cache --upgrade ca-certificates ca-certificates-bundle && update-ca-certificates

WORKDIR /app
COPY ./packages/brain/tls ./tls

Expand Down
4 changes: 3 additions & 1 deletion Dockerfile.brain.dev
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
ARG DOCKER_IMAGE=node:24.21.0-alpine
ARG DOCKER_IMAGE=node:24.22.1-alpine3.24
FROM ${DOCKER_IMAGE} AS build-stage

RUN apk add --no-cache --upgrade ca-certificates ca-certificates-bundle && update-ca-certificates

ENV NODE_ENV=development
WORKDIR /app

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"type": "module",
"private": true,
"engines": {
"node": ">=24.11.0"
"node": ">=24.22.1"
},
"workspaces": [
"packages/*"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
allowedOriginsFromEnv: string | string[] | null;
}): http.Server {
const app = express();
app.use(express.json());
app.use(express.json({ inflate: false }));
app.use(cors({ origin: allowedOriginsFromEnv ?? allowedOrigins }));

Check warning

Code scanning / CodeQL

Permissive CORS configuration Medium

CORS Origin allows broad access due to
permissive or user controlled value
.
CORS Origin allows broad access due to permissive or user controlled value.
CORS Origin allows broad access due to permissive or user controlled value.

app.use(createBrainValidatorsRouter({ brainDb }));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
allowedOriginsFromEnv: string | string[] | null;
}): http.Server {
const app = express();
app.use(express.json({ limit: RPC_MAX_PAYLOAD_BYTES }));
app.use(express.json({ limit: RPC_MAX_PAYLOAD_BYTES, inflate: false }));
app.use(cors({ origin: allowedOriginsFromEnv ?? allowedOrigins }));

Check warning

Code scanning / CodeQL

Permissive CORS configuration Medium

CORS Origin allows broad access due to
permissive or user controlled value
.
CORS Origin allows broad access due to permissive or user controlled value.
CORS Origin allows broad access due to permissive or user controlled value.

app.use(createKeystoresRouter({ reloadValidatorsCronTask, brainDb, network, validatorApi, signerApi, signerUrl }));
app.use(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@
// Express
app.use(
cors({
origin: allowedOriginsFromEnv ?? allowedOrigins(network)

Check warning

Code scanning / CodeQL

Permissive CORS configuration Medium

CORS Origin allows broad access due to
permissive or user controlled value
.
CORS Origin allows broad access due to permissive or user controlled value.
CORS Origin allows broad access due to permissive or user controlled value.
})
);
app.use(express.json());
app.use(express.json({ inflate: false }));
app.use(express.static(uiBuildPath));
// path-to-regexp (used by express >=5.0.0) has a breaking change
// where it does not allow to use wildcard among other characters
Expand Down
142 changes: 142 additions & 0 deletions packages/brain/test/unit/modules/jsonBodyEncoding.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { expect } from "chai";
import sinon from "sinon";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import http from "node:http";
import type { AddressInfo } from "node:net";
import { once } from "node:events";
import { brotliCompressSync } from "node:zlib";
import { ConsensusClient, ExecutionClient, Network } from "@stakingbrain/common";
import { startBrainApi } from "../../../src/modules/apiServers/brain/startBrainApi.js";
import { startLaunchpadApi } from "../../../src/modules/apiServers/launchpad/startLaunchpadApi.js";
import { startUiServer } from "../../../src/modules/apiServers/ui/startUiServer.js";
import {
BeaconchainApi,
BlockExplorerApi,
PostgresClient,
ValidatorApi,
Web3SignerApi
} from "../../../src/modules/apiClients/index.js";
import { BrainDataBase } from "../../../src/modules/db/index.js";
import { CronJob } from "../../../src/modules/cron/cron.js";
import { params } from "../../../src/params.js";

function postJson(server: http.Server, body: Buffer, encoding?: string): Promise<number> {
const address = server.address() as AddressInfo;
return new Promise((resolve, reject) => {
const request = http.request(
{
hostname: "127.0.0.1",
port: address.port,
path: "/",
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": body.length,
...(encoding ? { "Content-Encoding": encoding } : {})
}
},
(response) => {
response.resume();
response.on("end", () => resolve(response.statusCode!));
response.on("error", reject);
}
);
request.on("error", reject);
request.end(body);
});
}

describe("JSON request content encoding", () => {
const servers = new Map<string, http.Server>();
let directory: string;

before(async () => {
directory = fs.mkdtempSync(path.join(os.tmpdir(), "stakingbrain-json-encoding-"));
fs.writeFileSync(path.join(directory, "index.html"), "<html><head></head><body></body></html>");
for (const port of ["brainPort", "launchpadPort", "uiPort"] as const) sinon.stub(params, port).value(0);
const brainDb = sinon.createStubInstance(BrainDataBase);
const validatorApi = sinon.createStubInstance(ValidatorApi);
const signerApi = sinon.createStubInstance(Web3SignerApi);
const beaconchainApi = sinon.createStubInstance(BeaconchainApi);
const reloadValidatorsCronTask = sinon.createStubInstance(CronJob);
const signerUrl = "http://127.0.0.1:1";
const allowedOriginsFromEnv = null;
servers.set("Brain", startBrainApi({ brainDb, allowedOriginsFromEnv }));
servers.set(
"Launchpad",
startLaunchpadApi({
brainDb,
validatorApi,
signerApi,
beaconchainApi,
reloadValidatorsCronTask,
signerUrl,
network: Network.Hoodi,
allowedOriginsFromEnv
})
);
servers.set(
"UI",
startUiServer({
brainDb,
validatorApi,
signerApi,
beaconchainApi,
reloadValidatorsCronTask,
blockExplorerApi: sinon.createStubInstance(BlockExplorerApi),
postgresClient: sinon.createStubInstance(PostgresClient),
uiBuildPath: directory,
allowedOriginsFromEnv,
brainConfig: {
chain: {
network: Network.Hoodi,
executionClient: ExecutionClient.Geth,
consensusClient: ConsensusClient.Teku,
isMevBoostSet: false,
minGenesisTime: 0,
secondsPerSlot: 12,
slotsPerEpoch: 32
},
apis: {
signerUrl,
validatorUrl: signerUrl,
beaconchainUrl: signerUrl,
executionClientUrl: signerUrl,
prometheusUrl: signerUrl,
dappmanagerUrl: signerUrl,
blockExplorerUrl: signerUrl,
postgresUrl: signerUrl,
token: "",
tlsCert: null,
host: "localhost",
cors: null
}
}
})
);
await Promise.all([...servers.values()].map((server) => once(server, "listening")));
});

after(async () => {
await Promise.all(
[...servers.values()].map(
(server) => new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())))
)
);
sinon.restore();
fs.rmSync(directory, { recursive: true, force: true });
});

for (const name of ["Brain", "Launchpad", "UI"]) {
it(`${name} rejects Brotli-compressed JSON with HTTP 415`, async () => {
const body = brotliCompressSync(Buffer.from('{"probe":true}'));
expect(await postJson(servers.get(name)!, body, "br")).to.equal(415);
});

it(`${name} lets identity JSON reach routing`, async () => {
expect(await postJson(servers.get(name)!, Buffer.from('{"probe":true}'), "identity")).to.equal(404);
});
}
});
Loading