Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial Archiver Service Implementation #1

Merged
merged 12 commits into from
Feb 15, 2024
22 changes: 22 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# To get started, copy this file to .env and set your beacon http endpoint

BLOB_ARCHIVER_L1_BEACON_HTTP=<unset>
BLOB_ARCHIVER_DATA_STORE=s3
BLOB_ARCHIVER_S3_ENDPOINT=172.17.0.1:9000
BLOB_ARCHIVER_S3_ACCESS_KEY=admin
BLOB_ARCHIVER_S3_SECRET_ACCESS_KEY=password
BLOB_ARCHIVER_S3_ENDPOINT_HTTPS=false
BLOB_ARCHIVER_S3_BUCKET=blobs
BLOB_ARCHIVER_METRICS_ENABLED=true
BLOB_ARCHIVER_METRICS_PORT=7300
BLOB_ARCHIVER_ORIGIN_BLOCK=0x0

BLOB_API_L1_BEACON_HTTP=<unset>
BLOB_API_DATA_STORE=s3
BLOB_API_S3_ENDPOINT=172.17.0.1:9000
BLOB_API_S3_ACCESS_KEY=admin
BLOB_API_S3_SECRET_ACCESS_KEY=password
BLOB_API_S3_ENDPOINT_HTTPS=false
BLOB_API_S3_BUCKET=blobs
BLOB_API_METRICS_ENABLED=true
BLOB_API_METRICS_PORT=7301
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.idea/
.DS_Store
.swp
.env
api/bin
archiver/bin
18 changes: 18 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM golang:1.21.6-alpine3.19 as builder

RUN apk add --no-cache make gcc musl-dev linux-headers jq bash

WORKDIR /app

COPY ./go.mod ./go.sum /app/

RUN go mod download

COPY . /app

RUN make build

FROM alpine:3.19

COPY --from=builder /app/archiver/bin/blob-archiver /usr/local/bin/blob-archiver
COPY --from=builder /app/api/bin/blob-api /usr/local/bin/blob-api
35 changes: 35 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
build:
make -C ./archiver blob-archiver
make -C ./api blob-api
.PHONY: build

build-docker:
docker-compose build
.PHONY: build-docker

clean:
make -C ./archiver clean
make -C ./api clean
.PHONY: clean

test:
make -C ./archiver test
make -C ./api test
.PHONY: test

integration:
docker-compose down
docker-compose up -d minio create-buckets
RUN_INTEGRATION_TESTS=true go test -v ./...
.PHONY: integration

fmt:
gofmt -s -w .
.PHONY: fmt

check: fmt clean build build-docker lint test integration
.PHONY: check

lint:
golangci-lint run -E goimports,sqlclosecheck,bodyclose,asciicheck,misspell,errorlint --timeout 5m -e "errors.As" -e "errors.Is" ./...
.PHONY: lint
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,58 @@
# Blob Archiver
The Blob Archiver is a service to archive and query all historical blobs from the beacon chain. It consistens of two
components:

* **Archiver** - Tracks the beacon chain and writes blobs to a storage backend
* **API** - Implements the blob sidecars [API](https://ethereum.github.io/beacon-APIs/#/Beacon/getBlobSidecars), which
allows clients to retrieve blobs from the storage backend

### Storage
There are currently two supported storage options:

* On-disk storage - Blobs are written to disk in a directory
* S3 storage - Blobs are written to an S3 bucket

You can control which storage backend is used by setting the `BLOB_API_DATA_STORE` and `BLOB_ARCHIVER_DATA_STORE` to
either `disk` or `s3`.

### Data Validity
Currently, the archiver and api do not validate the beacon node's data. Therefore, it's important to either trust the
Beacon node, or validate the data in the client. There is an open [issue](https://github.com/base-org/blob-archiver/issues/4)
to add data validation to the archiver and api.

### Development
The `Makefile` contains a number of commands for development:

```sh
# Run the tests
make test
# Run the integration tests (will start a local S3 bucket)
make integration

# Lint the project
make lint

# Build the project
make build

# Check all tests, formatting, building
make check
```

#### Run Locally
To run the project locally, you should first copy `.env.template` to `.env` and then modify the environment variables
to your beacon client and storage backend of choice. Then you can run the project with:

```sh
docker-compose up
```

You can see a full list of configuration options by running:
```sh
# API
go run api/cmd/main.go

# Archiver
go run archiver/cmd/main.go

```
13 changes: 13 additions & 0 deletions api/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
blob-api:
env GO111MODULE=on GOOS=$(TARGETOS) GOARCH=$(TARGETARCH) go build -v $(LDFLAGS) -o ./bin/blob-api ./cmd/main.go

clean:
rm -f bin/blob-api

test:
go test -v -race ./...

.PHONY: \
blob-api \
clean \
test
72 changes: 72 additions & 0 deletions api/cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package main

import (
"context"
"fmt"
"os"

"github.com/base-org/blob-archiver/api/flags"
"github.com/base-org/blob-archiver/api/metrics"
"github.com/base-org/blob-archiver/api/service"
"github.com/base-org/blob-archiver/common/beacon"
"github.com/base-org/blob-archiver/common/storage"
opservice "github.com/ethereum-optimism/optimism/op-service"
"github.com/ethereum-optimism/optimism/op-service/cliapp"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
"github.com/ethereum/go-ethereum/log"
"github.com/urfave/cli/v2"
)

var (
Version = "v0.0.1"
GitCommit = ""
GitDate = ""
)

func main() {
oplog.SetupDefaults()

app := cli.NewApp()
app.Flags = cliapp.ProtectFlags(flags.Flags)
app.Version = opservice.FormatVersion(Version, GitCommit, GitDate, "")
app.Name = "blob-api"
app.Usage = "API service for Ethereum blobs"
app.Description = "Service for fetching blob sidecars from a datastore"
app.Action = cliapp.LifecycleCmd(Main())

err := app.Run(os.Args)
if err != nil {
log.Crit("Application failed", "message", err)
}
}

// Main is the entrypoint into the API.
// This method returns a cliapp.LifecycleAction, to create an op-service CLI-lifecycle-managed API Server.
func Main() cliapp.LifecycleAction {
return func(cliCtx *cli.Context, closeApp context.CancelCauseFunc) (cliapp.Lifecycle, error) {
cfg := flags.ReadConfig(cliCtx)
if err := cfg.Check(); err != nil {
return nil, fmt.Errorf("config check failed: %w", err)
}

l := oplog.NewLogger(oplog.AppOut(cliCtx), cfg.LogConfig)
oplog.SetGlobalLogHandler(l.GetHandler())
opservice.ValidateEnvVars(flags.EnvVarPrefix, flags.Flags, l)

m := metrics.NewMetrics()

storageClient, err := storage.NewStorage(cfg.StorageConfig, l)
if err != nil {
return nil, err
}

beaconClient, err := beacon.NewBeaconClient(context.Background(), cfg.BeaconConfig)
if err != nil {
return nil, err
}

l.Info("Initializing API Service")
api := service.NewAPI(storageClient, beaconClient, m, l)
return service.NewService(l, api, cfg, m.Registry()), nil
}
}
45 changes: 45 additions & 0 deletions api/flags/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package flags

import (
"fmt"

common "github.com/base-org/blob-archiver/common/flags"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/urfave/cli/v2"
)

type APIConfig struct {
LogConfig oplog.CLIConfig
MetricsConfig opmetrics.CLIConfig
BeaconConfig common.BeaconConfig
StorageConfig common.StorageConfig

ListenAddr string
}

func (c APIConfig) Check() error {
if err := c.StorageConfig.Check(); err != nil {
return err
}

if err := c.BeaconConfig.Check(); err != nil {
return err
}

if c.ListenAddr == "" {
return fmt.Errorf("listen address must be set")
}

return nil
}

func ReadConfig(cliCtx *cli.Context) APIConfig {
return APIConfig{
LogConfig: oplog.ReadCLIConfig(cliCtx),
MetricsConfig: opmetrics.ReadCLIConfig(cliCtx),
BeaconConfig: common.NewBeaconConfig(cliCtx),
StorageConfig: common.NewStorageConfig(cliCtx),
ListenAddr: cliCtx.String(ListenAddressFlag.Name),
}
}
34 changes: 34 additions & 0 deletions api/flags/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package flags

import (
common "github.com/base-org/blob-archiver/common/flags"
opservice "github.com/ethereum-optimism/optimism/op-service"
oplog "github.com/ethereum-optimism/optimism/op-service/log"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/urfave/cli/v2"
)

const EnvVarPrefix = "BLOB_API"

var (
ListenAddressFlag = &cli.StringFlag{
Name: "api-listen-address",
Usage: "The address to list for new requests on",
EnvVars: opservice.PrefixEnvVar(EnvVarPrefix, "LISTEN_ADDRESS"),
Value: "0.0.0.0:8000",
}
)

func init() {
var flags []cli.Flag

flags = append(flags, common.CLIFlags(EnvVarPrefix)...)
flags = append(flags, opmetrics.CLIFlags(EnvVarPrefix)...)
flags = append(flags, oplog.CLIFlags(EnvVarPrefix)...)
flags = append(flags, ListenAddressFlag)

Flags = flags
}

// Flags contains the list of configuration options available to the binary.
var Flags []cli.Flag
48 changes: 48 additions & 0 deletions api/metrics/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package metrics

import (
"github.com/ethereum-optimism/optimism/op-service/metrics"
opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics"
"github.com/prometheus/client_golang/prometheus"
)

type BlockIdType string

var (
MetricsNamespace = "blob_api"

BlockIdTypeHash BlockIdType = "hash"
BlockIdTypeBeacon BlockIdType = "beacon"
BlockIdTypeInvalid BlockIdType = "invalid"
)

type Metricer interface {
Registry() *prometheus.Registry
RecordBlockIdType(t BlockIdType)
}

type metricsRecorder struct {
inputType *prometheus.CounterVec
registry *prometheus.Registry
}

func NewMetrics() Metricer {
registry := opmetrics.NewRegistry()
factory := metrics.With(registry)
return &metricsRecorder{
registry: registry,
inputType: factory.NewCounterVec(prometheus.CounterOpts{
Namespace: MetricsNamespace,
Name: "block_id_type",
Help: "The type of block id used to request a block",
}, []string{"type"}),
}
}

func (m *metricsRecorder) RecordBlockIdType(t BlockIdType) {
m.inputType.WithLabelValues(string(t)).Inc()
}

func (m *metricsRecorder) Registry() *prometheus.Registry {
return m.registry
}
Loading