Skip to content

Commit 33c7c1f

Browse files
committed
Require non-empty measurements by default
1 parent 12b1924 commit 33c7c1f

File tree

9 files changed

+110
-61
lines changed

9 files changed

+110
-61
lines changed

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

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

Makefile

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,26 @@ build: ## Build the HTTP server
3838

3939
##@ Test & Development
4040

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

46-
.PHONY: dev-postgres-down
47-
dev-postgres-down: ## Stop the PostgreSQL database for development
47+
.PHONY: dev-postgres-stop
48+
dev-postgres-stop: ## Stop the PostgreSQL database for development
4849
docker rm -f postgres-test
4950

50-
.PHONY: dev-docker-compose-up
51-
dev-docker-compose-up: ## Start Docker compose
51+
.PHONY: dev-postgres-restart
52+
dev-postgres-restart: dev-postgres-stop dev-postgres-start ## Restart the PostgreSQL database for development
53+
54+
.PHONY: dev-docker-compose-start
55+
dev-docker-compose-start: ## Start Docker compose
5256
docker compose -f docker/docker-compose.yaml build
5357
docker compose -f docker/docker-compose.yaml up -d
5458

55-
.PHONY: dev-docker-compose-down
56-
dev-docker-compose-down: ## Stop Docker compose
59+
.PHONY: dev-docker-compose-stop
60+
dev-docker-compose-stop: ## Stop Docker compose
5761
docker compose -f docker/docker-compose.yaml down
5862

5963
.PHONY: lt
@@ -74,6 +78,10 @@ test: ## Run tests
7478
test-with-db: ## Run tests including live database tests
7579
RUN_DB_TESTS=1 go test -race ./...
7680

81+
.PHONY: integration-tests
82+
integration-tests: ## Run integration tests
83+
./scripts/ci/integration-test.sh
84+
7785
.PHONY: lint
7886
lint: ## Run linters
7987
gofmt -d -s .
@@ -115,7 +123,7 @@ db-dump: ## Dump the database contents to file 'database.dump'
115123
.PHONY: dev-db-setup
116124
dev-db-setup: ## Create the basic database entries for testing and development
117125
@printf "$(BLUE)Create the allow-all measurements $(NC)\n"
118-
$(CURL) $(CURL_AUTH) --request POST --url http://localhost:8081/api/admin/v1/measurements --data '{"measurement_id": "test1","attestation_type": "test","measurements": {}}'
126+
$(CURL) $(CURL_AUTH) --request POST --url http://localhost:8081/api/admin/v1/measurements --data '{"measurement_id": "test1","attestation_type": "test","measurements": {"11": {"expected": "efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f7"}}}'
119127

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

README.md

Lines changed: 23 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -51,26 +51,27 @@ curl -u admin:secret http://localhost:8081/api/admin/v1/measurements
5151

5252
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.
5353

54+
For development/testing, you may also allow empty measurements with `--enable-empty-measurements` or `ENABLE_EMPTY_MEASUREMENTS=1`. In production, keep this disabled and specify at least one expected measurement.
55+
5456
### Manual setup
5557

56-
**Start the database and the server:**
58+
See [`docs/devenv-setup.md`](./docs/devenv-setup.md) for more information on building and running BuilderHub locally with docker compose.
5759

58-
```bash
59-
# Start a Postgres database container
60-
docker run -d --name postgres-test -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=postgres postgres
60+
### Testing
6161

62-
# Apply the DB migrations
63-
for file in schema/*.sql; do psql "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable" -f $file; done
62+
Run the test suite with database tests included:
6463

65-
# Start the server
66-
go run cmd/httpserver/main.go
67-
```
64+
```bash
65+
# Run tests with real database integration
66+
make dev-postgres-start
67+
make test-with-db
6868

69-
### Using Docker
69+
# Run e2e integration tests, using the docker-compose setup
70+
make integration-tests
71+
```
7072

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

7576
### Example requests
7677

@@ -84,34 +85,25 @@ curl localhost:8080/api/l1-builder/v1/configuration
8485
curl -X POST localhost:8080/api/l1-builder/v1/register_credentials/rbuilder
8586
```
8687

87-
### Testing
88-
89-
Run test suite with database tests included:
90-
91-
```bash
92-
RUN_DB_TESTS=1 make test
93-
```
94-
9588
---
9689

9790
## Contributing
9891

9992
**Install dev dependencies**
10093

101-
```bash
102-
go install mvdan.cc/gofumpt@v0.4.0
103-
go install honnef.co/go/tools/cmd/staticcheck@v0.4.2
104-
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.60.3
105-
go install go.uber.org/nilaway/cmd/nilaway@v0.0.0-20240821220108-c91e71c080b7
106-
go install github.com/daixiang0/gci@v0.11.2
107-
```
94+
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)
10895

10996
**Lint, test, format**
11097

11198
```bash
112-
make lint
113-
make test
99+
# Quick and easy linting and testing without database
100+
make lt # stands for "make lint and test"
101+
102+
# Run things manually
114103
make fmt
104+
make lint
105+
make dev-postgres-start
106+
make test-with-db
115107
```
116108

117109
---
@@ -230,18 +222,7 @@ Payload
230222
}
231223
```
232224

233-
Note that only the measurements given are expected, and any non-present will be ignored.
234-
235-
To allow _any_ measurement, use an empty measurements field:
236-
`"measurements": {}`.
237-
238-
```json
239-
{
240-
"measurement_id": "test-blanket-allow",
241-
"attestation_type": "azure-tdx",
242-
"measurements": {}
243-
}
244-
```
225+
Note that only the measurements given are expected, and any non-present will be ignored. The `measurements` object must contain at least one entry.
245226

246227
### Enable/disable measurements
247228

cmd/httpserver/main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ var flags = []cli.Flag{
113113
Usage: "Use inmemory secrets service for testing",
114114
EnvVars: []string{"MOCK_SECRETS"},
115115
},
116+
&cli.BoolFlag{
117+
Name: "enable-empty-measurements",
118+
Usage: "allow empty measurements in AddMeasurement (local development/testing only)",
119+
EnvVars: []string{"ENABLE_EMPTY_MEASUREMENTS"},
120+
},
116121
}
117122

118123
func main() {
@@ -141,6 +146,7 @@ func runCli(cCtx *cli.Context) error {
141146
enablePprof := cCtx.Bool("pprof")
142147
drainDuration := time.Duration(cCtx.Int64("drain-seconds")) * time.Second
143148
mockSecretsStorage := cCtx.Bool("mock-secrets")
149+
enableEmptyMeasurements := cCtx.Bool("enable-empty-measurements")
144150
adminBasicUser := cCtx.String("admin-basic-user")
145151
adminPasswordBcrypt := cCtx.String("admin-basic-password-bcrypt")
146152
disableAdminAuth := cCtx.Bool("disable-admin-auth")
@@ -189,7 +195,7 @@ func runCli(cCtx *cli.Context) error {
189195
builderHub := application.NewBuilderHub(db, sm)
190196
builderHandler := ports.NewBuilderHubHandler(builderHub, log)
191197

192-
adminHandler := ports.NewAdminHandler(db, sm, log)
198+
adminHandler := ports.NewAdminHandler(db, sm, log, enableEmptyMeasurements)
193199
cfg := &httpserver.HTTPServerConfig{
194200
ListenAddr: listenAddr,
195201
MetricsAddr: metricsAddr,

docker/docker-compose.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ services:
4242
INTERNAL_ADDR: "0.0.0.0:8082"
4343
METRICS_ADDR: "0.0.0.0:8090"
4444
DISABLE_ADMIN_AUTH: "1" # local dev only; do not use in production
45+
ENABLE_EMPTY_MEASUREMENTS: "1" # local dev/testing convenience
4546

4647
proxy:
4748
image: flashbots/builder-hub-mock-proxy

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ require (
1616
github.com/stretchr/testify v1.9.0
1717
github.com/urfave/cli/v2 v2.27.2
1818
go.uber.org/atomic v1.11.0
19+
golang.org/x/crypto v0.27.0
1920
)
2021

2122
require (
@@ -31,7 +32,6 @@ require (
3132
github.com/valyala/fastrand v1.1.0 // indirect
3233
github.com/valyala/histogram v1.2.0 // indirect
3334
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
34-
golang.org/x/crypto v0.27.0 // indirect
3535
golang.org/x/sys v0.25.0 // indirect
3636
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
3737
gopkg.in/yaml.v3 v3.0.1 // indirect

httpserver/e2e_test.go

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,49 @@ func createMeasurement(t *testing.T, s *Server, measurement ports.Measurement) {
308308
})
309309
}
310310

311+
func Test_AddMeasurement_EmptyRejected(t *testing.T) {
312+
if os.Getenv("RUN_DB_TESTS") != "1" {
313+
t.Skip("skipping test; RUN_DB_TESTS is not set to 1")
314+
}
315+
s, _, _ := createServer(t)
316+
empty := ports.Measurement{
317+
Name: "empty-measurement",
318+
AttestationType: "test",
319+
Measurements: map[string]domain.SingleMeasurement{},
320+
}
321+
status, _ := execRequestNoAuth(t, s.GetAdminRouter(), http.MethodPost, "/api/admin/v1/measurements", empty, nil)
322+
require.Equal(t, http.StatusBadRequest, status)
323+
}
324+
325+
func Test_AddMeasurement_EmptyAllowedWithFlag(t *testing.T) {
326+
if os.Getenv("RUN_DB_TESTS") != "1" {
327+
t.Skip("skipping test; RUN_DB_TESTS is not set to 1")
328+
}
329+
// Create a server where empty measurements are allowed
330+
dbService := createDbService(t)
331+
mss := domain.NewMockSecretService()
332+
bhs := application.NewBuilderHub(dbService, mss)
333+
bhh := ports.NewBuilderHubHandler(bhs, getTestLogger())
334+
ah := ports.NewAdminHandler(dbService, mss, getTestLogger(), true)
335+
s, err := NewHTTPServer(&HTTPServerConfig{
336+
DrainDuration: latency,
337+
ListenAddr: listenAddr,
338+
InternalAddr: internalAddr,
339+
AdminAddr: adminAddr,
340+
AdminAuthDisabled: true,
341+
Log: getTestLogger(),
342+
}, bhh, ah)
343+
require.NoError(t, err)
344+
345+
empty := ports.Measurement{
346+
Name: "empty-allowed",
347+
AttestationType: "test",
348+
Measurements: map[string]domain.SingleMeasurement{},
349+
}
350+
status, _ := execRequestNoAuth(t, s.GetAdminRouter(), http.MethodPost, "/api/admin/v1/measurements", empty, nil)
351+
require.Equal(t, http.StatusOK, status)
352+
}
353+
311354
func createDbService(t *testing.T) *database.Service {
312355
t.Helper()
313356
dbService, err := database.NewDatabaseService("postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable")
@@ -328,7 +371,7 @@ func createServer(t *testing.T) (*Server, *database.Service, *domain.InmemorySec
328371
bhs := application.NewBuilderHub(dbService, mss)
329372
bhh := ports.NewBuilderHubHandler(bhs, getTestLogger())
330373
_ = bhh
331-
ah := ports.NewAdminHandler(dbService, mss, getTestLogger())
374+
ah := ports.NewAdminHandler(dbService, mss, getTestLogger(), false)
332375
_ = ah
333376
s, err := NewHTTPServer(&HTTPServerConfig{
334377
DrainDuration: latency,

httpserver/handler_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func Test_Handlers_Healthcheck_Drain_Undrain(t *testing.T) {
4141
InternalAddr: internalAddr,
4242
AdminAddr: adminAddr,
4343
Log: getTestLogger(),
44-
}, ports.NewBuilderHubHandler(nil, getTestLogger()), ports.NewAdminHandler(nil, nil, getTestLogger()))
44+
}, ports.NewBuilderHubHandler(nil, getTestLogger()), ports.NewAdminHandler(nil, nil, getTestLogger(), false))
4545
require.NoError(t, err)
4646

4747
{ // Check health

ports/admin_handler.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,14 @@ type AdminSecretService interface {
2828
}
2929

3030
type AdminHandler struct {
31-
builderService AdminBuilderService
32-
secretService AdminSecretService
33-
log *httplog.Logger
31+
builderService AdminBuilderService
32+
secretService AdminSecretService
33+
log *httplog.Logger
34+
allowEmptyMeasurements bool
3435
}
3536

36-
func NewAdminHandler(service AdminBuilderService, secretService AdminSecretService, log *httplog.Logger) *AdminHandler {
37-
return &AdminHandler{builderService: service, secretService: secretService, log: log}
37+
func NewAdminHandler(service AdminBuilderService, secretService AdminSecretService, log *httplog.Logger, allowEmptyMeasurements bool) *AdminHandler {
38+
return &AdminHandler{builderService: service, secretService: secretService, log: log, allowEmptyMeasurements: allowEmptyMeasurements}
3839
}
3940

4041
func (s *AdminHandler) GetActiveConfigForBuilder(w http.ResponseWriter, r *http.Request) {
@@ -96,6 +97,13 @@ func (s *AdminHandler) AddMeasurement(w http.ResponseWriter, r *http.Request) {
9697
w.WriteHeader(http.StatusBadRequest)
9798
return
9899
}
100+
// Disallow empty measurements map unless explicitly allowed
101+
if len(measurement.Measurements) == 0 && !s.allowEmptyMeasurements {
102+
s.log.Error("measurements field must not be empty")
103+
w.WriteHeader(http.StatusBadRequest)
104+
_, _ = w.Write([]byte("measurements must contain at least one entry"))
105+
return
106+
}
99107
err = s.builderService.AddMeasurement(r.Context(), toDomainMeasurement(measurement), false)
100108
if err != nil {
101109
s.log.Error("failed to add measurement", "error", err)

0 commit comments

Comments
 (0)