Skip to content
Open
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
6 changes: 3 additions & 3 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ jobs:
- name: Run migrations
run: for file in schema/*.sql; do psql "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable" -f $file; done

- name: Run unit tests and generate the coverage report
run: RUN_DB_TESTS=1 make test-race
- name: Run unit tests
run: make test-with-db

lint:
name: Lint
Expand All @@ -61,7 +61,7 @@ jobs:
run: go install honnef.co/go/tools/cmd/staticcheck@2025.1.1

- name: Install golangci-lint
run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.8
run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.2

- name: Install NilAway
run: go install go.uber.org/nilaway/cmd/nilaway@v0.0.0-20240821220108-c91e71c080b7
Expand Down
18 changes: 16 additions & 2 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
version: "2"
run:
tests: false
linters:
Expand All @@ -6,7 +7,6 @@ linters:
- cyclop
- forbidigo
- funlen
- gci
- gochecknoglobals
- gochecknoinits
- gocritic
Expand Down Expand Up @@ -43,7 +43,6 @@ linters:
#
# Disabled because deprecated:
#
- tenv

linters-settings:
#
Expand Down Expand Up @@ -91,3 +90,18 @@ linters-settings:
- 'MockBeaconClient'
- 'RelayAPI'
- 'Webserver'
formatters:
enable:
- gci
- gofmt
- gofumpt
- goimports
settings:
gofumpt:
extra-rules: true
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
40 changes: 40 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Repository Guidelines

## Project Structure & Modules
- `cmd/httpserver/`: CLI entrypoint and service startup.
- `httpserver/`: HTTP server, handlers, routing, and tests.
- `application/`, `domain/`, `ports/`, `adapters/`: Clean/hexagonal layers (core logic, interfaces, DB/secrets adapters).
- `schema/`: SQL migrations; `testdata/`: fixtures; `metrics/`: Prometheus metrics; `docker/`: Dockerfiles and compose; `scripts/`: CI/e2e helpers.

## Build, Test, and Dev Commands
- `make build`: Build the server to `build/builder-hub`.
- `go run cmd/httpserver/main.go`: Run locally (see env flags below).
- `make test`: Run unit tests. Use `RUN_DB_TESTS=1 make test` to include DB/e2e tests.
- `make cover` / `make cover-html`: Coverage summary / HTML report.
- `make fmt`: Format and tidy imports/modules.
- `make lint`: `go vet`, `staticcheck`, `golangci-lint`, and format checks.
- `make docker-httpserver`: Build the Docker image.
- Helpful: `docs/devenv-setup.md`, `scripts/ci/integration-test.sh`.

## Coding Style & Conventions
- Go formatting is enforced: run `make fmt` (gofmt, gofumpt, gci, go mod tidy).
- Always run `make fmt` and `make lint`.
- If touching `httpserver/e2e_test.go`, always test with database enabled: `make dev-postgres-restart test-with-db dev-postgres-stop`.
- Package names: lower-case; exported symbols: PascalCase; locals: camelCase.
- Keep files focused; group handlers in `httpserver`, business logic in `application/domain`.
- Imports: standard → third-party → local (enforced by gci).
- Indentation: Use tab, not spaces

## Testing Guidelines
- Framework: Go `testing`; tests live in `*_test.go`. Use `testdata/` fixtures.
- Quick run: `make test`; with DB/e2e: `RUN_DB_TESTS=1 make test`.
- Coverage: `make cover`. Prefer table-driven tests and clear Arrange/Act/Assert sections.

## Commit & Pull Request Guidelines
- Commits: concise, imperative mood (e.g., "Add API spec tests"); optional scope tags (e.g., `[CI]`); reference issues/PRs when relevant.
- PRs: include description, linked issues, testing steps (curl examples for API changes), and screenshots/log snippets when useful.
- Required before merge: `make lt` (lint + tests) green, updated docs in `docs/` when API/behavior changes.

## Configuration & Security
- Common env flags (via CLI/env): `LISTEN_ADDR`, `ADMIN_ADDR`, `INTERNAL_ADDR`, `METRICS_ADDR`, `POSTGRES_DSN`, `LOG_JSON`, `LOG_DEBUG`, `PPROF`, `MOCK_SECRETS`, `AWS_BUILDER_CONFIGS_SECRET_NAME`/`_PREFIX`.
- Default DSN targets local Postgres; do not commit secrets. Use `MOCK_SECRETS=1` for local dev.
49 changes: 40 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

VERSION := $(shell git describe --tags --always --dirty="-dev")

# Admin API auth for curl examples (set ADMIN_AUTH="admin:secret" or similar. can be empty when server is run with --disable-admin-auth)
ADMIN_AUTH ?=
CURL ?= curl -v
CURL_AUTH := $(if $(ADMIN_AUTH),-u $(ADMIN_AUTH),)

# A few colors
RED:=\033[0;31m
BLUE:=\033[0;34m
Expand Down Expand Up @@ -33,6 +38,28 @@ build: ## Build the HTTP server

##@ Test & Development

.PHONY: dev-postgres-start
dev-postgres-start: ## Start the PostgreSQL database for development
docker run -d --name postgres-test -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=postgres postgres
sleep 3
for file in schema/*.sql; do psql "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable" -f $$file; done

.PHONY: dev-postgres-stop
dev-postgres-stop: ## Stop the PostgreSQL database for development
docker rm -f postgres-test

.PHONY: dev-postgres-restart
dev-postgres-restart: dev-postgres-stop dev-postgres-start ## Restart the PostgreSQL database for development

.PHONY: dev-docker-compose-start
dev-docker-compose-start: ## Start Docker compose
docker compose -f docker/docker-compose.yaml build
docker compose -f docker/docker-compose.yaml up -d

.PHONY: dev-docker-compose-stop
dev-docker-compose-stop: ## Stop Docker compose
docker compose -f docker/docker-compose.yaml down

.PHONY: lt
lt: lint test ## Run linters and tests (always do this!)

Expand All @@ -45,12 +72,16 @@ fmt: ## Format the code (use this often)

.PHONY: test
test: ## Run tests
go test ./...

.PHONY: test-race
test-race: ## Run tests with race detector
go test -race ./...

.PHONY: test-with-db
test-with-db: ## Run tests including live database tests
RUN_DB_TESTS=1 go test -race ./...

.PHONY: integration-tests
integration-tests: ## Run integration tests
./scripts/ci/integration-test.sh

.PHONY: lint
lint: ## Run linters
gofmt -d -s .
Expand Down Expand Up @@ -92,16 +123,16 @@ db-dump: ## Dump the database contents to file 'database.dump'
.PHONY: dev-db-setup
dev-db-setup: ## Create the basic database entries for testing and development
@printf "$(BLUE)Create the allow-all measurements $(NC)\n"
curl -v --request POST --url http://localhost:8081/api/admin/v1/measurements --data '{"measurement_id": "test1","attestation_type": "test","measurements": {}}'
$(CURL) $(CURL_AUTH) --request POST --url http://localhost:8081/api/admin/v1/measurements --data '{"measurement_id": "test1","attestation_type": "test","measurements": {}}'

@printf "$(BLUE)Enable the measurements $(NC)\n"
curl -v --request POST --url http://localhost:8081/api/admin/v1/measurements/activation/test1 --data '{"enabled": true}'
$(CURL) $(CURL_AUTH) --request POST --url http://localhost:8081/api/admin/v1/measurements/activation/test1 --data '{"enabled": true}'

@printf "$(BLUE)Create the builder $(NC)\n"
curl -v --request POST --url http://localhost:8081/api/admin/v1/builders --data '{"name": "test_builder","ip_address": "1.2.3.4", "network": "production"}'
$(CURL) $(CURL_AUTH) --request POST --url http://localhost:8081/api/admin/v1/builders --data '{"name": "test_builder","ip_address": "1.2.3.4", "network": "production"}'

@printf "$(BLUE)Create the builder configuration $(NC)\n"
curl -v --request POST --url http://localhost:8081/api/admin/v1/builders/configuration/test_builder --data '{"dns_name": "foobar-v1.a.b.c","rbuilder": {"extra_data": "FooBar"}}'
$(CURL) $(CURL_AUTH) --request POST --url http://localhost:8081/api/admin/v1/builders/configuration/test_builder --data '{"dns_name": "foobar-v1.a.b.c","rbuilder": {"extra_data": "FooBar"}}'

@printf "$(BLUE)Enable the builder $(NC)\n"
curl -v --request POST --url http://localhost:8081/api/admin/v1/builders/activation/test_builder --data '{"enabled": true}'
$(CURL) $(CURL_AUTH) --request POST --url http://localhost:8081/api/admin/v1/builders/activation/test_builder --data '{"enabled": true}'
92 changes: 51 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,56 @@ BuilderHub has these responsibilities:
## Getting started


### Manual setup
### Admin authentication

The Admin API (port 8081) requires HTTP Basic Auth. Configure via env vars or flags:

- `ADMIN_BASIC_USER` (default: `admin`)
- `ADMIN_BASIC_PASSWORD_BCRYPT` (bcrypt hash of the password; required)

**Start the database and the server:**
Generate a bcrypt hash (example using htpasswd):

```bash
# Start a Postgres database container
docker run -d --name postgres-test -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=postgres postgres
htpasswd -nbBC 10 "" 'secret' | cut -d: -f2
```

# Apply the DB migrations
for file in schema/*.sql; do psql "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable" -f $file; done
Run with:

# Start the server
```bash
export ADMIN_BASIC_USER=admin
export ADMIN_BASIC_PASSWORD_BCRYPT='$2y$12$...'
go run cmd/httpserver/main.go
```

### Using Docker
Use Basic Auth when calling admin endpoints, e.g.:

```bash
curl -u admin:secret http://localhost:8081/api/admin/v1/measurements
```

Local development only: you can disable Admin API auth with `--disable-admin-auth` or `DISABLE_ADMIN_AUTH=1`. This is unsafe; never use in production.

For development/testing, you may also allow empty measurements with `--allow-empty-measurements` or `ALLOW_EMPTY_MEASUREMENTS=1`. In production, keep this disabled and specify at least one expected measurement.

### Manual setup

See [`docs/devenv-setup.md`](./docs/devenv-setup.md) for more information on building and running BuilderHub locally with docker compose.

### Testing

Run the test suite with database tests included:

```bash
# Run tests with real database integration
make dev-postgres-start
make test-with-db

# Run e2e integration tests, using the docker-compose setup
make integration-tests
```

- See instructions on using Docker to run the full stack at [`docs/devenv-setup.md`](./docs/devenv-setup.md)
- Also check out the [`docker-compose.yaml`](../docker/docker-compose.yaml) file, which sets up the BuilderHub, a mock proxy, and a Postgres database.
- Finally, there's an e2e api spec test suite you can run: [`./scripts/ci/integration-test.sh`](./scripts/ci/integration-test.sh)
The [integration tests](./scripts/ci/integration-test.sh) use the above mentioned [docker-compose setup](./docker/docker-compose.yaml)
and [Hurl](https://hurl.dev) for API request automation (see the [code here](./scripts/ci/e2e-test.hurl)).

### Example requests

Expand All @@ -55,34 +85,25 @@ curl localhost:8080/api/l1-builder/v1/configuration
curl -X POST localhost:8080/api/l1-builder/v1/register_credentials/rbuilder
```

### Testing

Run test suite with database tests included:

```bash
RUN_DB_TESTS=1 make test
```

---

## Contributing

**Install dev dependencies**

```bash
go install mvdan.cc/gofumpt@v0.4.0
go install honnef.co/go/tools/cmd/staticcheck@v0.4.2
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.60.3
go install go.uber.org/nilaway/cmd/nilaway@v0.0.0-20240821220108-c91e71c080b7
go install github.com/daixiang0/gci@v0.11.2
```
Use the same development dependencies as Github CI via [checks.yml](https://github.com/flashbots/builder-hub/blob/required-measurements/.github/workflows/checks.yml#L44-L77)

**Lint, test, format**

```bash
make lint
make test
# Quick and easy linting and testing without database
make lt # stands for "make lint and test"

# Run things manually
make fmt
make lint
make dev-postgres-start
make test-with-db
```

---
Expand Down Expand Up @@ -201,18 +222,7 @@ Payload
}
```

Note that only the measurements given are expected, and any non-present will be ignored.

To allow _any_ measurement, use an empty measurements field:
`"measurements": {}`.

```json
{
"measurement_id": "test-blanket-allow",
"attestation_type": "azure-tdx",
"measurements": {}
}
```
Note that only the measurements given are expected, and any non-present will be ignored. The `measurements` object must contain at least one entry.

### Enable/disable measurements

Expand Down Expand Up @@ -287,4 +297,4 @@ Payload: JSON with secrets, both flattened/unflattened
{
...
}
```
```
26 changes: 13 additions & 13 deletions adapters/database/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ func NewDatabaseService(dsn string) (*Service, error) {
return nil, err
}

db.DB.SetMaxOpenConns(50)
db.DB.SetMaxIdleConns(10)
db.DB.SetConnMaxIdleTime(0)
db.SetMaxOpenConns(50)
db.SetMaxIdleConns(10)
db.SetConnMaxIdleTime(0)

dbService := &Service{DB: db} //nolint:exhaustruct
return dbService, err
Expand Down Expand Up @@ -117,9 +117,9 @@ func (s *Service) RegisterCredentialsForBuilder(ctx context.Context, builderName
}

_, err = tx.Exec(`
INSERT INTO service_credential_registrations
INSERT INTO service_credential_registrations
(builder_name, service, tls_cert, ecdsa_pubkey, is_active, measurement_id)
VALUES ($1, $2, $3, $4, true,
VALUES ($1, $2, $3, $4, true,
(SELECT id FROM measurements_whitelist WHERE name = $5 AND attestation_type = $6)
)
`, builderName, service, nullableTLSCert, ecdsaPubKey, measurementName, attestationType)
Expand Down Expand Up @@ -150,26 +150,26 @@ func (s *Service) GetActiveConfigForBuilder(ctx context.Context, builderName str

func (s *Service) GetActiveBuildersWithServiceCredentials(ctx context.Context, network string) ([]domain.BuilderWithServices, error) {
rows, err := s.DB.QueryContext(ctx, `
SELECT
SELECT
b.name,
b.ip_address,
b.dns_name,
scr.service,
scr.tls_cert,
scr.ecdsa_pubkey
FROM
FROM
builders b
LEFT JOIN
LEFT JOIN
service_credential_registrations scr ON b.name = scr.builder_name
WHERE
WHERE
b.is_active = true AND (scr.is_active = true OR scr.is_active IS NULL) AND b.network = $1
ORDER BY
ORDER BY
b.name, scr.service
`, network)
if err != nil {
return nil, err
}
defer rows.Close()
defer rows.Close() //nolint:errcheck

buildersMap := make(map[string]*BuilderWithCredentials)

Expand Down Expand Up @@ -227,9 +227,9 @@ func (s *Service) GetActiveBuildersWithServiceCredentials(ctx context.Context, n
func (s *Service) LogEvent(ctx context.Context, eventName, builderName, name string) error {
// Insert new event log entry with a subquery to fetch the measurement_id
_, err := s.DB.ExecContext(ctx, `
INSERT INTO event_log
INSERT INTO event_log
(event_name, builder_name, measurement_id)
VALUES ($1, $2,
VALUES ($1, $2,
(SELECT id FROM measurements_whitelist WHERE name = $3)
)
`, eventName, builderName, name)
Expand Down
Loading