Skip to content

Commit 155b465

Browse files
committed
first version
logo and docker
0 parents  commit 155b465

20 files changed

Lines changed: 4743 additions & 0 deletions

.dockerignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# keep the build context tiny — only source goes to the daemon
2+
data/
3+
sakura
4+
sakura-static
5+
*.o
6+
*.tar.gz
7+
.git/
8+
tests/
9+
*.md

.github/workflows/docker.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: docker
2+
3+
on:
4+
push:
5+
branches: [master]
6+
tags: ["v*"]
7+
pull_request:
8+
branches: [master]
9+
10+
env:
11+
REGISTRY: ghcr.io
12+
IMAGE_NAME: ${{ github.repository }}
13+
14+
jobs:
15+
build:
16+
runs-on: ubuntu-latest
17+
permissions:
18+
contents: read
19+
packages: write
20+
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@v4
24+
25+
- name: Set up Docker Buildx
26+
uses: docker/setup-buildx-action@v3
27+
28+
- name: Log in to GHCR
29+
if: github.event_name != 'pull_request'
30+
uses: docker/login-action@v3
31+
with:
32+
registry: ${{ env.REGISTRY }}
33+
username: ${{ github.actor }}
34+
password: ${{ secrets.GITHUB_TOKEN }}
35+
36+
- name: Extract image metadata
37+
id: meta
38+
uses: docker/metadata-action@v5
39+
with:
40+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
41+
tags: |
42+
type=ref,event=branch
43+
type=ref,event=pr
44+
type=semver,pattern={{version}}
45+
type=semver,pattern={{major}}.{{minor}}
46+
type=sha
47+
type=raw,value=latest,enable={{is_default_branch}}
48+
49+
- name: Build and push
50+
uses: docker/build-push-action@v6
51+
with:
52+
context: .
53+
# The Dockerfile fetches the odin-linux-amd64 toolchain, so the
54+
# image is amd64-only for now.
55+
platforms: linux/amd64
56+
push: ${{ github.event_name != 'pull_request' }}
57+
tags: ${{ steps.meta.outputs.tags }}
58+
labels: ${{ steps.meta.outputs.labels }}
59+
cache-from: type=gha
60+
cache-to: type=gha,mode=max

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
sakura
2+
data/

Dockerfile

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# syntax=docker/dockerfile:1
2+
#
3+
# Smallest-possible image for sakura: the server is dependency-free and links
4+
# fully static, so the final stage is `FROM scratch` — no base OS, no libc, no
5+
# shell. Just the ~1.3 MB stripped binary and an empty /data.
6+
#
7+
# docker build -t sakura .
8+
# docker run --rm -p 3000:3000 -v sakura-data:/data sakura
9+
#
10+
# Override the toolchain version with --build-arg ODIN_VERSION=dev-2026-05.
11+
12+
# ---- build stage: fetch the Odin toolchain and produce a static binary ----
13+
FROM debian:bookworm-slim AS build
14+
ARG ODIN_VERSION=dev-2026-05
15+
RUN apt-get update && apt-get install -y --no-install-recommends \
16+
ca-certificates curl clang lld libc6-dev \
17+
&& rm -rf /var/lib/apt/lists/*
18+
19+
WORKDIR /opt/odin
20+
RUN curl -fsSL -o /tmp/odin.tar.gz \
21+
"https://github.com/odin-lang/Odin/releases/download/${ODIN_VERSION}/odin-linux-amd64-${ODIN_VERSION}.tar.gz" \
22+
&& tar xzf /tmp/odin.tar.gz -C /opt/odin --strip-components=1 \
23+
&& rm /tmp/odin.tar.gz
24+
ENV PATH="/opt/odin:${PATH}"
25+
26+
WORKDIR /src
27+
COPY . .
28+
# Fully static link so the binary needs nothing at runtime.
29+
RUN odin build . -out:/tmp/sakura -o:speed -extra-linker-flags:"-static" \
30+
&& strip /tmp/sakura \
31+
&& /tmp/sakura --selftest
32+
33+
# ---- prep stage: an empty, correctly-owned /data (scratch has no mkdir) ----
34+
FROM busybox:latest AS prep
35+
RUN mkdir -p /data
36+
37+
# ---- final stage: nothing but the binary ----
38+
FROM scratch
39+
COPY --from=prep --chown=65532:65532 /data /data
40+
COPY --from=build /tmp/sakura /sakura
41+
42+
ENV SAKURA_HOST=0.0.0.0 \
43+
SAKURA_PORT=3000 \
44+
SAKURA_DATA=/data
45+
EXPOSE 3000
46+
VOLUME ["/data"]
47+
USER 65532:65532
48+
ENTRYPOINT ["/sakura"]

README.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
<p align="center">
2+
<img src="assets/logo.png" width="200" alt="Sakura logo">
3+
</p>
4+
5+
# Sakura
6+
7+
A dependency-free [Blossom](https://github.com/hzrd149/blossom) server written in Odin.
8+
9+
## What it implements
10+
11+
| BUD | Coverage |
12+
|-----|----------|
13+
| **BUD-01** | `GET`/`HEAD /<sha256>`, RFC 7233 range requests (`206`, `Content-Range`, `Accept-Ranges`), full CORS, `X-Reason` diagnostics, content sniffing |
14+
| **BUD-02** | `PUT /upload`, blob descriptors, `200`/`201` semantics, `X-SHA-256` check |
15+
| **BUD-04** | `PUT /mirror` (http sources; fetch, hash-verify, store) |
16+
| **BUD-06** | `HEAD /upload` upload-requirements probe (`X-SHA-256`/`X-Content-Length`/`X-Content-Type`) |
17+
| **BUD-11** | `Authorization: Nostr <base64url>` kind-24242 tokens: recomputes the event id, verifies the Schnorr signature, checks `kind`, `created_at`, `expiration`, `t`, `x` and `server` tags |
18+
| **BUD-12** | `GET /list/<pubkey>`, `DELETE /<sha256>` |
19+
20+
## Performance notes
21+
22+
- Reads are lock-free: `GET`/`HEAD` stat and stream blob files straight off disk
23+
in 64 KiB chunks, and the index mutex is only taken for mutations and metadata.
24+
- Worker threads (one per core) all accept on a single shared listening socket,
25+
with HTTP keep-alive and request pipelining.
26+
- Each request runs on an arena allocator that is reset afterwards, so parsing
27+
and response building cause no per-request heap churn.
28+
- secp256k1 verification is variable-time. It touches only public data, so
29+
constant time isn't needed, and dropping it buys speed. Field reduction
30+
exploits the `p = 2^256 - 2^32 - 977` shape; scalars and points use 4×`u64`
31+
limbs with `u128` intermediates.
32+
- SHA-256 is streaming and hashes directly out of the upload buffer.
33+
34+
## Build
35+
36+
```sh
37+
odin build . -out:sakura -o:speed
38+
```
39+
40+
## Run
41+
42+
```sh
43+
SAKURA_PORT=3000 SAKURA_DATA=./data ./sakura
44+
```
45+
46+
Configuration (environment variables):
47+
48+
| Variable | Default | Meaning |
49+
|----------|---------|---------|
50+
| `SAKURA_HOST` | `0.0.0.0` | bind address |
51+
| `SAKURA_PORT` | `3000` | listen port |
52+
| `SAKURA_DATA` | `./data` | blob + index directory |
53+
| `SAKURA_DOMAIN` | _(off)_ | domain for BUD-11 `server`-tag scoping |
54+
| `SAKURA_PUBLIC_URL` | _(derive from Host)_ | base URL in descriptors |
55+
| `SAKURA_MAX_MB` | `100` | max blob size (MB) |
56+
| `SAKURA_WORKERS` | `16` | worker threads |
57+
| `SAKURA_REQUIRE_AUTH` | `true` | require auth for upload/delete/mirror |
58+
59+
## Tests
60+
61+
```sh
62+
./sakura --selftest # in-binary known-answer tests (47 checks, exit code = result)
63+
./tests/run.sh # the above + SHA-256 differential + live HTTP end-to-end
64+
```
65+
66+
`--selftest` covers:
67+
68+
- SHA-256: the canonical KATs (empty, `"abc"`, the two NIST multi-block
69+
vectors) plus the one-million-`'a'` streaming/padding stress.
70+
- secp256k1 units: `fe_inv`/`fe_sqrt`/`fe_neg` round-trips, the published
71+
x-coordinates of `2G`/`3G`, `(n-1)G == -G`, `2G+G == 3G`, `lift_x(Gx)`.
72+
- BIP-340 verify: all 19 vectors from the spec CSV, including every must-fail
73+
edge case (off-curve key, `has_even_y(R)` false, negated message/`s`,
74+
infinite `sG−eP` with `R.x` = 0 and 1, `sig[0:32]` off-curve / `== p`,
75+
`sig[32:64] == n`, pubkey `> p`) and the 0/1/17/100-byte message vectors.
76+
- BIP-340 sign: byte-exact signatures for the `aux_rand == 0` spec vectors
77+
(the deterministic case our minter uses).
78+
79+
`tests/run.sh` also diffs `--sha256 <file>` against the system `sha256sum`
80+
across length boundaries (0,1,…55,56,…63,64,65,…127,128,… plus random sizes)
81+
and drives a live server through upload / re-upload / no-auth / wrong-x-tag /
82+
GET / HEAD / range / list / expiry / wrong-verb / delete / OPTIONS.
83+
84+
## Tooling commands
85+
86+
```sh
87+
./sakura --pubkey <seckey_hex> # derive x-only pubkey
88+
./sakura --make-token <seckey_hex> <verb> [hash] [exp_unix] # mint a kind-24242 token
89+
./sakura --sha256 <file> # hash a file (for diffing)
90+
```
91+
92+
Example end-to-end upload:
93+
94+
```sh
95+
HASH=$(sha256sum file.png | cut -d' ' -f1)
96+
TOK=$(./sakura --make-token <seckey> upload "$HASH")
97+
curl -X PUT --data-binary @file.png -H "Authorization: Nostr $TOK" http://localhost:3000/upload
98+
```
99+
100+
## Docker
101+
102+
Because the binary is dependency-free and links fully static, the image is
103+
`FROM scratch`: no base OS, no libc, no shell. The only non-zero layer is the
104+
~1.2 MB stripped binary; everything else is an empty `/data` dir and metadata.
105+
106+
```sh
107+
docker build -t sakura . # downloads Odin, static-builds, runs --selftest
108+
docker run --rm -p 3000:3000 -v sakura-data:/data sakura
109+
```
110+
111+
- The container runs as non-root (`uid 65532`) and persists blobs in the
112+
`/data` volume; configure it with the same `SAKURA_*` env vars, e.g.
113+
`-e SAKURA_REQUIRE_AUTH=false`.
114+
- The build stage runs `--selftest` and fails the build if any vector fails.
115+
Pin the toolchain with `--build-arg ODIN_VERSION=dev-2026-05`.
116+
117+
To test the live image, `tests/docker.sh` builds it, runs a container, drives
118+
the full Blossom flow, and checks persistence across container replacement.
119+
It mints tokens with the image's _own_ CLI, so it validates only the shipped
120+
artifact:
121+
122+
```sh
123+
tests/docker.sh # build + black-box test the container
124+
SKIP_BUILD=1 tests/docker.sh # reuse an already-built image
125+
```
126+
127+
Resulting image is ~1.7 MB locally (≈0.5 MB compressed). To shrink the binary
128+
further you can `upx --best` it in the build stage (≈0.6 MB) at the cost of a
129+
small startup decompression; that's left out by default to keep cold-start
130+
instant.
131+
132+
## Layout
133+
134+
| File | Responsibility |
135+
|------|----------------|
136+
| `sha256.odin` | streaming SHA-256 |
137+
| `secp256k1.odin` | field/scalar/point math, BIP-340 verify + sign |
138+
| `codec.odin` | hex, Base64-URL |
139+
| `json.odin` | JSON parser + NIP-01 string escaping |
140+
| `nostr.odin` | event parsing, id recomputation, BUD-11 validation, token minting |
141+
| `store.odin` | sharded on-disk blob store + append-only index |
142+
| `http.odin` | request parser, response/streaming writer |
143+
| `http_client.odin` | minimal HTTP client for `/mirror` |
144+
| `server.odin` | routing and every endpoint |
145+
| `main.odin` | config, worker pool, self-test, CLI |
146+
147+
## Limitations
148+
149+
- `/mirror` fetches `http://` sources only; TLS is out of scope for a
150+
dependency-free build, so `https://` sources return `502`.
151+
- BUD-05 (media optimization), BUD-07 (payments), BUD-08/09/10 are not server
152+
endpoints and are intentionally out of scope.

assets/logo.png

46.6 KB
Loading

assets/logo.svg

Lines changed: 46 additions & 0 deletions
Loading

build.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/sh
2+
set -e
3+
odin build . -out:sakura -o:speed

0 commit comments

Comments
 (0)