Skip to content

Commit 0fa146d

Browse files
authored
Merge pull request #1 from base-org/initial-archiver
Initial Archiver Service Implementation
2 parents 9253741 + 3714928 commit 0fa146d

37 files changed

+3286
-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

+57
Original file line numberDiff line numberDiff line change
@@ -1 +1,58 @@
11
# Blob Archiver
2+
The Blob Archiver is a service to archive and allow querying of all historical blobs from the beacon chain. It consists
3+
of two 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+
### Data Validity
19+
Currently, the archiver and api do not validate the beacon node's data. Therefore, it's important to either trust the
20+
Beacon node, or validate the data in the client. There is an open [issue](https://github.com/base-org/blob-archiver/issues/4)
21+
to add data validation to the archiver and api.
22+
23+
### Development
24+
The `Makefile` contains a number of commands for development:
25+
26+
```sh
27+
# Run the tests
28+
make test
29+
# Run the integration tests (will start a local S3 bucket)
30+
make integration
31+
32+
# Lint the project
33+
make lint
34+
35+
# Build the project
36+
make build
37+
38+
# Check all tests, formatting, building
39+
make check
40+
```
41+
42+
#### Run Locally
43+
To run the project locally, you should first copy `.env.template` to `.env` and then modify the environment variables
44+
to your beacon client and storage backend of choice. Then you can run the project with:
45+
46+
```sh
47+
docker-compose up
48+
```
49+
50+
You can see a full list of configuration options by running:
51+
```sh
52+
# API
53+
go run api/cmd/main.go
54+
55+
# Archiver
56+
go run archiver/cmd/main.go
57+
58+
```

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

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

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 fmt.Errorf("storage config check failed: %w", err)
24+
}
25+
26+
if err := c.BeaconConfig.Check(); err != nil {
27+
return fmt.Errorf("beacon config check failed: %w", 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

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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-listen-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+
Flags = append(Flags, common.CLIFlags(EnvVarPrefix)...)
24+
Flags = append(Flags, opmetrics.CLIFlags(EnvVarPrefix)...)
25+
Flags = append(Flags, oplog.CLIFlags(EnvVarPrefix)...)
26+
Flags = append(Flags, ListenAddressFlag)
27+
}
28+
29+
// Flags contains the list of configuration options available to the binary.
30+
var Flags []cli.Flag

api/metrics/metrics.go

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

0 commit comments

Comments
 (0)