Skip to content

Commit 4977c6a

Browse files
danyalproutqiwu7
andcommitted
Initial implementation of blob archiver service
Co-authored-by: Qi Wu <[email protected]>
1 parent 9253741 commit 4977c6a

35 files changed

+3066
-0
lines changed

.env.template

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# To get started, copy this file to .env and set your beacon http endpoint
2+
3+
BLOB_ARCHIVER_L1_BEACON_HTTP=<unset>
4+
BLOB_ARCHIVER_DATA_STORE=s3
5+
BLOB_ARCHIVER_S3_ENDPOINT=172.17.0.1:9000
6+
BLOB_ARCHIVER_S3_ACCESS_KEY=admin
7+
BLOB_ARCHIVER_S3_SECRET_ACCESS_KEY=password
8+
BLOB_ARCHIVER_S3_ENDPOINT_HTTPS=false
9+
BLOB_ARCHIVER_S3_BUCKET=blobs
10+
BLOB_ARCHIVER_METRICS_ENABLED=true
11+
BLOB_ARCHIVER_METRICS_PORT=7300
12+
BLOB_ARCHIVER_ORIGIN_BLOCK=0x0
13+
14+
BLOB_API_L1_BEACON_HTTP=<unset>
15+
BLOB_API_DATA_STORE=s3
16+
BLOB_API_S3_ENDPOINT=172.17.0.1:9000
17+
BLOB_API_S3_ACCESS_KEY=admin
18+
BLOB_API_S3_SECRET_ACCESS_KEY=password
19+
BLOB_API_S3_ENDPOINT_HTTPS=false
20+
BLOB_API_S3_BUCKET=blobs
21+
BLOB_API_METRICS_ENABLED=true
22+
BLOB_API_METRICS_PORT=7301

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.idea/
2+
.DS_Store
3+
.swp
4+
.env
5+
api/bin
6+
archiver/bin

Dockerfile

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
FROM golang:1.21.6-alpine3.19 as builder
2+
3+
RUN apk add --no-cache make gcc musl-dev linux-headers jq bash
4+
5+
WORKDIR /app
6+
7+
COPY ./go.mod ./go.sum /app/
8+
9+
RUN go mod download
10+
11+
COPY . /app
12+
13+
RUN make build
14+
15+
FROM alpine:3.19
16+
17+
COPY --from=builder /app/archiver/bin/blob-archiver /usr/local/bin/blob-archiver
18+
COPY --from=builder /app/api/bin/blob-api /usr/local/bin/blob-api

Makefile

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
build:
2+
make -C ./archiver blob-archiver
3+
make -C ./api blob-api
4+
.PHONY: build
5+
6+
build-docker:
7+
docker-compose build
8+
.PHONY: build-docker
9+
10+
clean:
11+
make -C ./archiver clean
12+
make -C ./api clean
13+
.PHONY: clean
14+
15+
test:
16+
make -C ./archiver test
17+
make -C ./api test
18+
.PHONY: test
19+
20+
integration:
21+
docker-compose down
22+
docker-compose up -d minio create-buckets
23+
RUN_INTEGRATION_TESTS=true go test -v ./...
24+
.PHONY: integration
25+
26+
fmt:
27+
gofmt -s -w .
28+
.PHONY: fmt
29+
30+
check: fmt clean build build-docker lint test integration
31+
.PHONY: check
32+
33+
lint:
34+
golangci-lint run -E goimports,sqlclosecheck,bodyclose,asciicheck,misspell,errorlint --timeout 5m -e "errors.As" -e "errors.Is" ./...
35+
.PHONY: lint

README.md

+52
Original file line numberDiff line numberDiff line change
@@ -1 +1,53 @@
11
# Blob Archiver
2+
The Blob Archiver is a service to archive and query all historical blobs from the beacon chain. It consistens of two
3+
components:
4+
5+
* **Archiver** - Tracks the beacon chain and writes blobs to a storage backend
6+
* **API** - Implements the blob sidecars [API](https://ethereum.github.io/beacon-APIs/#/Beacon/getBlobSidecars), which
7+
allows clients to retrieve blobs from the storage backend
8+
9+
### Storage
10+
There are currently two supported storage options:
11+
12+
* On-disk storage - Blobs are written to disk in a directory
13+
* S3 storage - Blobs are written to an S3 bucket
14+
15+
You can control which storage backend is used by setting the `BLOB_API_DATA_STORE` and `BLOB_ARCHIVER_DATA_STORE` to
16+
either `disk` or `s3`.
17+
18+
### Development
19+
The `Makefile` contains a number of commands for development:
20+
21+
```sh
22+
# Run the tests
23+
make test
24+
# Run the integration tests (will start a local S3 bucket)
25+
make integration
26+
27+
# Lint the project
28+
make lint
29+
30+
# Build the project
31+
make build
32+
33+
# Check all tests, formatting, building
34+
make check
35+
```
36+
37+
#### Run Locally
38+
To run the project locally, you should first copy `.env.template` to `.env` and then modify the environment variables
39+
to your beacon client and storage backend of choice. Then you can run the project with:
40+
41+
```sh
42+
docker-compose up
43+
```
44+
45+
You can see a full list of configuration options by running:
46+
```sh
47+
# API
48+
go run api/cmd/main.go
49+
50+
# Archiver
51+
go run archiver/cmd/main.go
52+
53+
```

api/Makefile

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
blob-api:
2+
env GO111MODULE=on GOOS=$(TARGETOS) GOARCH=$(TARGETARCH) go build -v $(LDFLAGS) -o ./bin/blob-api ./cmd/main.go
3+
4+
clean:
5+
rm -f bin/blob-api
6+
7+
test:
8+
go test -v -race ./...
9+
10+
.PHONY: \
11+
blob-api \
12+
clean \
13+
test

api/cmd/main.go

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
"github.com/base-org/blob-archiver/api/flags"
9+
"github.com/base-org/blob-archiver/api/metrics"
10+
"github.com/base-org/blob-archiver/api/service"
11+
"github.com/base-org/blob-archiver/common/beacon"
12+
"github.com/base-org/blob-archiver/common/storage"
13+
opservice "github.com/ethereum-optimism/optimism/op-service"
14+
"github.com/ethereum-optimism/optimism/op-service/cliapp"
15+
oplog "github.com/ethereum-optimism/optimism/op-service/log"
16+
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
17+
"github.com/ethereum/go-ethereum/log"
18+
"github.com/urfave/cli/v2"
19+
)
20+
21+
var (
22+
Version = "v0.0.1"
23+
GitCommit = ""
24+
GitDate = ""
25+
)
26+
27+
func main() {
28+
oplog.SetupDefaults()
29+
30+
app := cli.NewApp()
31+
app.Flags = cliapp.ProtectFlags(flags.Flags)
32+
app.Version = opservice.FormatVersion(Version, GitCommit, GitDate, "")
33+
app.Name = "blob-api"
34+
app.Usage = "API service for Ethereum blobs"
35+
app.Description = "Service for fetching blob sidecars from a datastore"
36+
app.Action = cliapp.LifecycleCmd(Main())
37+
38+
err := app.Run(os.Args)
39+
if err != nil {
40+
log.Crit("Application failed", "message", err)
41+
}
42+
}
43+
44+
// Main is the entrypoint into the API.
45+
// This method returns a cliapp.LifecycleAction, to create an op-service CLI-lifecycle-managed API Server.
46+
func Main() cliapp.LifecycleAction {
47+
return func(cliCtx *cli.Context, closeApp context.CancelCauseFunc) (cliapp.Lifecycle, error) {
48+
cfg := flags.ReadConfig(cliCtx)
49+
if err := cfg.Check(); err != nil {
50+
return nil, fmt.Errorf("config check failed: %w", err)
51+
}
52+
53+
l := oplog.NewLogger(oplog.AppOut(cliCtx), cfg.LogConfig)
54+
oplog.SetGlobalLogHandler(l.GetHandler())
55+
opservice.ValidateEnvVars(flags.EnvVarPrefix, flags.Flags, l)
56+
57+
registry := opmetrics.NewRegistry()
58+
m := metrics.NewMetrics(registry)
59+
60+
storageClient, err := storage.NewStorage(cfg.StorageConfig, l)
61+
if err != nil {
62+
return nil, err
63+
}
64+
65+
beaconClient, err := beacon.NewBeaconClient(context.Background(), cfg.BeaconConfig)
66+
if err != nil {
67+
return nil, err
68+
}
69+
70+
l.Info("Initializing API Service")
71+
return service.NewAPIService(l, storageClient, beaconClient, cfg, registry, m), nil
72+
}
73+
}

api/flags/config.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package flags
2+
3+
import (
4+
"fmt"
5+
6+
common "github.com/base-org/blob-archiver/common/flags"
7+
oplog "github.com/ethereum-optimism/optimism/op-service/log"
8+
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
9+
"github.com/urfave/cli/v2"
10+
)
11+
12+
type APIConfig struct {
13+
LogConfig oplog.CLIConfig
14+
MetricsConfig opmetrics.CLIConfig
15+
BeaconConfig common.BeaconConfig
16+
StorageConfig common.StorageConfig
17+
18+
ListenAddr string
19+
}
20+
21+
func (c APIConfig) Check() error {
22+
if err := c.StorageConfig.Check(); err != nil {
23+
return err
24+
}
25+
26+
if err := c.BeaconConfig.Check(); err != nil {
27+
return err
28+
}
29+
30+
if c.ListenAddr == "" {
31+
return fmt.Errorf("listen address must be set")
32+
}
33+
34+
return nil
35+
}
36+
37+
func ReadConfig(cliCtx *cli.Context) APIConfig {
38+
return APIConfig{
39+
LogConfig: oplog.ReadCLIConfig(cliCtx),
40+
MetricsConfig: opmetrics.ReadCLIConfig(cliCtx),
41+
BeaconConfig: common.NewBeaconConfig(cliCtx),
42+
StorageConfig: common.NewStorageConfig(cliCtx),
43+
ListenAddr: cliCtx.String(ListenAddressFlag.Name),
44+
}
45+
}

api/flags/flags.go

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package flags
2+
3+
import (
4+
common "github.com/base-org/blob-archiver/common/flags"
5+
opservice "github.com/ethereum-optimism/optimism/op-service"
6+
oplog "github.com/ethereum-optimism/optimism/op-service/log"
7+
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
8+
"github.com/urfave/cli/v2"
9+
)
10+
11+
const EnvVarPrefix = "BLOB_API"
12+
13+
var (
14+
ListenAddressFlag = &cli.StringFlag{
15+
Name: "api-list-address",
16+
Usage: "The address to list for new requests on",
17+
EnvVars: opservice.PrefixEnvVar(EnvVarPrefix, "LISTEN_ADDRESS"),
18+
Value: "0.0.0.0:8000",
19+
}
20+
)
21+
22+
func init() {
23+
var flags []cli.Flag
24+
25+
flags = append(flags, common.CLIFlags(EnvVarPrefix)...)
26+
flags = append(flags, opmetrics.CLIFlags(EnvVarPrefix)...)
27+
flags = append(flags, oplog.CLIFlags(EnvVarPrefix)...)
28+
flags = append(flags, ListenAddressFlag)
29+
30+
Flags = flags
31+
}
32+
33+
// Flags contains the list of configuration options available to the binary.
34+
var Flags []cli.Flag

api/metrics/metrics.go

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package metrics
2+
3+
import (
4+
"github.com/ethereum-optimism/optimism/op-service/metrics"
5+
"github.com/prometheus/client_golang/prometheus"
6+
)
7+
8+
type BlockIdType string
9+
10+
var (
11+
MetricsNamespace = "blob_api"
12+
13+
BlockIdTypeHash BlockIdType = "hash"
14+
BlockIdTypeBeacon BlockIdType = "beacon"
15+
BlockIdTypeInvalid BlockIdType = "invalid"
16+
)
17+
18+
type Metricer interface {
19+
RecordBlockIdType(t BlockIdType)
20+
}
21+
22+
type metricsRecorder struct {
23+
inputType *prometheus.CounterVec
24+
}
25+
26+
func NewMetrics(registry *prometheus.Registry) Metricer {
27+
factory := metrics.With(registry)
28+
return &metricsRecorder{
29+
inputType: factory.NewCounterVec(prometheus.CounterOpts{
30+
Namespace: MetricsNamespace,
31+
Name: "block_id_type",
32+
Help: "The type of block id used to request a block",
33+
}, []string{"type"}),
34+
}
35+
}
36+
37+
func (m *metricsRecorder) RecordBlockIdType(t BlockIdType) {
38+
m.inputType.WithLabelValues(string(t)).Inc()
39+
}

0 commit comments

Comments
 (0)