Skip to content
Closed
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
11 changes: 9 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
BIN := arkiv-storaged
CMD := ./cmd/arkiv-storaged
OUT := ./bin/$(BIN)
VERSION_PKG := github.com/Arkiv-Network/arkiv-storage-service/version

TAG ?= $(shell git describe --tags --abbrev=0 --always 2>/dev/null || echo unknown)
COMMIT ?= $(shell git rev-parse HEAD 2>/dev/null || echo unknown)
DIRTY ?= $(shell test -z "$$(git status --porcelain 2>/dev/null)" && echo false || echo true)
BUILD_TIME ?= $(shell date -u '+%Y-%m-%dT%H:%M:%SZ')
LDFLAGS := -X '$(VERSION_PKG).Tag=$(TAG)' -X '$(VERSION_PKG).Commit=$(COMMIT)' -X '$(VERSION_PKG).Dirty=$(DIRTY)' -X '$(VERSION_PKG).BuildTime=$(BUILD_TIME)'

.PHONY: build install test lint clean

build:
go build -o $(OUT) $(CMD)
go build -ldflags "$(LDFLAGS)" -o $(OUT) $(CMD)

install:
go install $(CMD)
go install -ldflags "$(LDFLAGS)" $(CMD)

test: build
go test ./...
Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,33 @@ make build # writes to ./bin/arkiv-storaged
make install # installs to $GOPATH/bin
```

`make build` and `make install` embed version metadata from the current git
checkout when available:

- `tag` from `git describe --tags --abbrev=0 --always`
- `commit` from `git rev-parse HEAD`
- `dirty` from whether the worktree has uncommitted changes
- `buildTime` from the current UTC time

For release or CI builds, the same fields can be set explicitly:

```sh
go build \
-ldflags "\
-X github.com/Arkiv-Network/arkiv-storage-service/version.Tag=v0.1.0 \
-X github.com/Arkiv-Network/arkiv-storage-service/version.Commit=$(git rev-parse HEAD) \
-X github.com/Arkiv-Network/arkiv-storage-service/version.Dirty=false \
-X github.com/Arkiv-Network/arkiv-storage-service/version.BuildTime=$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
-o ./bin/arkiv-storaged \
./cmd/arkiv-storaged
```

Print the embedded version information without starting the daemon:

```sh
arkiv-storaged --version
```

Run it:

```sh
Expand All @@ -35,6 +62,7 @@ Flags:
-chain-addr listen address for the chain ingest server (default 127.0.0.1:2704)
-query-addr listen address for the query server (default 127.0.0.1:2705)
-data-dir path to the data directory (default ~/.arkiv-storaged)
-version print build version information and exit
```

### Configuration file
Expand All @@ -60,6 +88,16 @@ The service exposes two HTTP JSON-RPC 2.0 servers:
- `arkiv_getEntityByAddress` — fetch a single entity by address
- `arkiv_getEntityCount` — total number of live entities at the head

Both listeners also expose a plain HTTP version endpoint:

```sh
curl http://127.0.0.1:2704/version
curl http://127.0.0.1:2705/version
```

The response is JSON and includes the embedded tag, full commit, short commit,
dirty flag, build time, Go version, and any available Go VCS metadata.

## Development

```sh
Expand Down
17 changes: 15 additions & 2 deletions cmd/arkiv-storaged/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
Expand All @@ -15,6 +16,7 @@ import (
"github.com/Arkiv-Network/arkiv-storage-service/chain"
"github.com/Arkiv-Network/arkiv-storage-service/query"
"github.com/Arkiv-Network/arkiv-storage-service/store"
"github.com/Arkiv-Network/arkiv-storage-service/version"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethdb/pebble"
"gopkg.in/yaml.v2"
Expand Down Expand Up @@ -61,6 +63,7 @@ func main() {
chainAddr := flag.String("chain-addr", "127.0.0.1:2704", "address for the chain ingest JSON-RPC server (arkiv-op-reth → storaged)")
queryAddr := flag.String("query-addr", "127.0.0.1:2705", "address for the query JSON-RPC server (SDK → storaged)")
dataDir := flag.String("data-dir", defaultDataDir(), "path to the data directory (config.yaml read here; PebbleDB opened at <data-dir>/db)")
showVersion := flag.Bool("version", false, "print build version information and exit")

flag.Usage = func() {
fmt.Fprintf(os.Stderr, `arkiv-storaged — Arkiv entity storage daemon
Expand All @@ -79,6 +82,16 @@ Flags:

flag.Parse()

if *showVersion {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(version.Current()); err != nil {
fmt.Fprintf(os.Stderr, "print version: %v\n", err)
os.Exit(1)
}
return
}

log := slog.New(slog.NewTextHandler(os.Stderr, nil))

// Load config file from the data dir resolved so far.
Expand Down Expand Up @@ -125,8 +138,8 @@ Flags:
os.Exit(1)
}

chainHTTP := &http.Server{Addr: *chainAddr, Handler: chainSrv}
queryHTTP := &http.Server{Addr: *queryAddr, Handler: querySrv}
chainHTTP := &http.Server{Addr: *chainAddr, Handler: version.Handler(chainSrv)}
queryHTTP := &http.Server{Addr: *queryAddr, Handler: version.Handler(querySrv)}

// Start both servers.
go func() {
Expand Down
78 changes: 73 additions & 5 deletions integration/full_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"encoding/json"
"fmt"
"net"
"net/http"
"os"
"os/exec"
"sort"
Expand All @@ -15,13 +16,20 @@
"github.com/Arkiv-Network/arkiv-storage-service/chain"
"github.com/Arkiv-Network/arkiv-storage-service/query"
"github.com/Arkiv-Network/arkiv-storage-service/types"
"github.com/Arkiv-Network/arkiv-storage-service/version"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/rpc"
)

var binaryPath string

const (
testVersionTag = "v-test"
testVersionCommit = "0123456789abcdef0123456789abcdef01234567"
testVersionBuildTime = "2026-05-06T20:30:00Z"
)

func TestMain(m *testing.M) {
tmp, err := os.CreateTemp("", "arkiv-storaged-*")
if err != nil {
Expand All @@ -31,7 +39,13 @@
_ = tmp.Close()
binaryPath = tmp.Name()

build := exec.Command("go", "build", "-o", binaryPath, "../cmd/arkiv-storaged")
ldflags := fmt.Sprintf(
"-X github.com/Arkiv-Network/arkiv-storage-service/version.Tag=%s -X github.com/Arkiv-Network/arkiv-storage-service/version.Commit=%s -X github.com/Arkiv-Network/arkiv-storage-service/version.Dirty=true -X github.com/Arkiv-Network/arkiv-storage-service/version.BuildTime=%s",
testVersionTag,
testVersionCommit,
testVersionBuildTime,
)
build := exec.Command("go", "build", "-ldflags", ldflags, "-o", binaryPath, "../cmd/arkiv-storaged")
build.Stdout = os.Stderr
build.Stderr = os.Stderr
if err := build.Run(); err != nil {
Expand All @@ -56,7 +70,6 @@
iAddr2 = common.Address(iKey2[:20])
iAddr3 = common.Address(iKey3[:20])


iOwner1 = common.HexToAddress("0xaaaa000000000000000000000000000000000001")
iOwner2 = common.HexToAddress("0xaaaa000000000000000000000000000000000002")
iOwner3 = common.HexToAddress("0xaaaa000000000000000000000000000000000003")
Expand Down Expand Up @@ -150,8 +163,10 @@
// ----- test environment -----

type testEnv struct {
c *rpc.Client // chain client
q *rpc.Client // query client
c *rpc.Client // chain client
q *rpc.Client // query client
chainAddr string
queryAddr string
}

// freePort returns a free TCP port on localhost.
Expand Down Expand Up @@ -221,7 +236,60 @@
}
t.Cleanup(queryClient.Close)

return &testEnv{c: chainClient, q: queryClient}
return &testEnv{c: chainClient, q: queryClient, chainAddr: chainAddr, queryAddr: queryAddr}
}

func assertVersionInfo(t *testing.T, info version.Info) {
t.Helper()
if info.Tag != testVersionTag {
t.Fatalf("tag = %q, want %q", info.Tag, testVersionTag)
}
if info.Commit != testVersionCommit {
t.Fatalf("commit = %q, want %q", info.Commit, testVersionCommit)
}
if info.CommitShort != testVersionCommit[:12] {
t.Fatalf("commitShort = %q, want %q", info.CommitShort, testVersionCommit[:12])
}
if !info.Dirty {
t.Fatal("dirty = false, want true")
}
if info.BuildTime != testVersionBuildTime {
t.Fatalf("buildTime = %q, want %q", info.BuildTime, testVersionBuildTime)
}
if info.GoVersion == "" {
t.Fatal("goVersion is empty")
}
}

func TestVersionFlag(t *testing.T) {
out, err := exec.Command(binaryPath, "--version").Output()
if err != nil {
t.Fatalf("arkiv-storaged --version: %v", err)
}
var info version.Info
if err := json.Unmarshal(out, &info); err != nil {
t.Fatalf("decode version output: %v\n%s", err, out)
}
assertVersionInfo(t, info)
}

func TestVersionEndpoint(t *testing.T) {
env := newTestEnv(t)
for _, addr := range []string{env.chainAddr, env.queryAddr} {
resp, err := http.Get("http://" + addr + "/version")
if err != nil {
t.Fatalf("GET /version from %s: %v", addr, err)
}
defer resp.Body.Close()

Check failure on line 283 in integration/full_test.go

View workflow job for this annotation

GitHub Actions / ci

Error return value of `resp.Body.Close` is not checked (errcheck)
if resp.StatusCode != http.StatusOK {
t.Fatalf("GET /version from %s status = %d, want %d", addr, resp.StatusCode, http.StatusOK)
}
var info version.Info
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
t.Fatalf("decode /version from %s: %v", addr, err)
}
assertVersionInfo(t, info)
}
}

func (e *testEnv) commit(t *testing.T, blocks ...types.ArkivBlock) {
Expand Down
24 changes: 24 additions & 0 deletions version/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package version

import (
"encoding/json"
"net/http"
)

// Handler serves GET /version and delegates all other requests to next.
func Handler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/version" {
next.ServeHTTP(w, r)
return
}
if r.Method != http.MethodGet {
w.Header().Set("Allow", http.MethodGet)
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}

w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(Current())
})
}
55 changes: 55 additions & 0 deletions version/http_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package version

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)

func TestHandlerServesVersion(t *testing.T) {
oldTag := Tag
t.Cleanup(func() { Tag = oldTag })
Tag = "v-test"

handler := Handler(http.NotFoundHandler())
req := httptest.NewRequest(http.MethodGet, "/version", nil)
rec := httptest.NewRecorder()

handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
}

var info Info
if err := json.NewDecoder(rec.Body).Decode(&info); err != nil {
t.Fatalf("decode response: %v", err)
}
if info.Tag != "v-test" {
t.Fatalf("tag = %q, want %q", info.Tag, "v-test")
}
}

func TestHandlerDelegatesOtherPaths(t *testing.T) {
handler := Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusAccepted)
}))
req := httptest.NewRequest(http.MethodPost, "/", nil)
rec := httptest.NewRecorder()

handler.ServeHTTP(rec, req)
if rec.Code != http.StatusAccepted {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusAccepted)
}
}

func TestHandlerRejectsNonGetVersionRequests(t *testing.T) {
handler := Handler(http.NotFoundHandler())
req := httptest.NewRequest(http.MethodPost, "/version", nil)
rec := httptest.NewRecorder()

handler.ServeHTTP(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusMethodNotAllowed)
}
}
Loading
Loading