Skip to content

CIP-002: storage HTTP API + c0mpute storage CLI - #21

Merged
ralyodio merged 1 commit into
feat/storage-cipsfrom
feat/cip-002-storage-api
Aug 29, 2026
Merged

CIP-002: storage HTTP API + c0mpute storage CLI#21
ralyodio merged 1 commit into
feat/storage-cipsfrom
feat/cip-002-storage-api

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Implements CIP-002: turns the c0mpute-store engine into a usable service and puts it behind a CLI.

Stacked on #20 (the CIPs) — this PR targets feat/storage-cips, so its diff is implementation only.

Single node: every shard lands on the local disk and host_hint stays None. Cross-node placement is CIP-003.

What works

$ c0mpute storage put sample.bin
blake3:6820f4b461b2a0ad7b48a25b1fd6d3e7d93645b6a78f18feb8dc7beefcc9c040
  12.0 MiB in 3 block(s), 42 shards, tier standard (1.4x expansion, 16.8 MiB raw)

$ c0mpute storage info blake3:6820f4…
tier      standard — RS 10/14, 1.4x expansion, tolerates 4 shard losses
blocks    3 of 4.0 MiB
health    42 present, 0 missing — healthy

# delete 4 of 14 shards of block 0, then:
health    38 present, 4 missing — degraded (readable)
$ c0mpute storage get blake3:6820f4… -o out.bin   # byte-identical

Verified end to end against a running server, not just in tests: 12 MiB round-trips byte-identical over HTTP, Range reads match dd, 422/416/404/507 all behave, and an object still reconstructs after losing 4 shards.

c0mpute-store

  • Tier (hot / standard / critical) carrying (k, parity) and CIP-001 pricing. hot is RS with k=1, which makes the parity shards byte-identical to the data shard — genuine 3-copy replication with 1x repair amplification, which is the whole reason CIP-001 wants a replicated tier.
  • Block layer. Objects split into blocks, each independently RS-encoded. Memory is bounded by block size rather than object size, and a range read touches only the blocks it needs — CIP-007's random-access files cannot exist without this.
  • Manifest v2 (version, block_size, tier, blocks[]) with a shim that still parses v1, so the original tests stay meaningful.
  • block_size_for() scales 4 MiB → 256 MiB with object size. A 1 TiB object gets ~4096 blocks and a sub-megabyte manifest instead of ~262k blocks and tens of MB.
  • put_stream / read_stream / get_range; put is now a thin wrapper over put_stream so there is one write path, not two that drift.

c0mpute-gateway

  • /storage/v1/{objects,shards,manifests,status} — Range support, tier selection, idempotent PUT, single-flight per object hash, disk budget.
  • Commit-then-verify on every write. Bytes must hash to the hash in the URL or the write is rejected 422. This is what makes the store trustworthy without trusting the uploader.
  • ed25519 signed-request auth (DIP-0007 shape) bound to method + path + body hash with a 5-minute skew window. Writes require it; reads don't, because the hash is the capability.

c0mpute-cli — the sub-feature

c0mpute storage put|get|ls|info|verify|rm|status|tiers|serve, matching the cli = "c0mpute storage" that plugins/storage/module.toml has been declaring all along.

Commands that need later CIPs (volume, mount, provide) are absent rather than stubbed, and --help names the CIP that brings each one. --help never advertises something that doesn't work.

Two bugs found by running it, not by the tests

1. Data loss on rollback. A write that failed its hash commitment deleted every shard hash it had touched. Shards are content-addressed and shared, so re-uploading an existing object's bytes under a wrong committed hash produces identical shard hashes — the rollback deleted the intact object's shards. One malformed request, from anyone who can obtain the content, destroyed it.

ChunkStore::put_new now reports whether a call created a chunk, and rollback removes only what it created. Regression tests at both the store and HTTP level (the original unit test stored nothing beforehand, so there was nothing for the bad write to destroy — which is exactly why it passed). Worth remembering for CIP-004 and CIP-005, which both delete content-addressed data and will meet the same trap.

2. Tracing wrote to stdout, so HASH=$(c0mpute storage put f) captured log lines and every scripted use broke. Diagnostics now go to stderr; daemon mode is unaffected because daemonize_worker already points both streams at the same log file.

Also: Config's api/storage/gateway sections get serde defaults, so a partial config.toml setting only [storage] root loads instead of erroring on the sections you left out.

Testing

65 new tests — 31 store, 12 auth, 22 HTTP integration. Whole workspace green (25 suites), clippy-clean on the touched crates.

Note that repo CI runs only security scanners (CodeQL, ThreatCrush, gitleaks, semgrep, Socket, bun audit) — there is no Rust build/test job, so cargo test --workspace is worth running locally on review. Adding one might be worth a follow-up.

Not in scope

Cross-node placement (CIP-003), repair (CIP-005), payouts (CIP-006), the filesystem and mount (CIP-007 → CIP-009). Until CIP-003, erasure coding on a single node is overhead without durability — c0mpute storage status says so in as many words rather than implying the data is safe.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LsQAuvXkmyHTgnvquLHrRx

Implements CIP-002: turns the c0mpute-store engine into a usable service and
puts it behind a CLI. Single node — every shard lands on the local disk and
host_hint stays None; cross-node placement is CIP-003.

c0mpute-store
  - Tier (hot / standard / critical) carrying (k, parity) and CIP-001 pricing.
    hot is RS k=1, which makes parity shards byte-identical copies: real 3-copy
    replication with 1x repair amplification.
  - Block layer: objects split into blocks, each independently RS-encoded, so
    memory is bounded by block size and a range read touches only the blocks it
    needs. CIP-007's random-access files depend on this.
  - Manifest v2 (version, block_size, tier, blocks[]) with a shim that still
    parses v1.
  - block_size_for() scales 4 MiB -> 256 MiB with object size, keeping a 1 TiB
    object's manifest under a megabyte instead of ~262k blocks.
  - put_stream / read_stream / get_range. put is now a wrapper over put_stream
    so there is one write path.
  - Storage::list() for enumerating what a node holds.

c0mpute-gateway
  - /storage/v1/{objects,shards,manifests,status} with Range support, tier
    selection, idempotent PUT, single-flight per hash, and a disk budget.
  - Commit-then-verify on every write: bytes must hash to the hash the caller
    committed to, or 422.
  - ed25519 signed-request auth (DIP-0007 shape) bound to method + path + body
    hash, with a 5-minute skew window. Writes require it; reads do not, since
    the hash is the capability.

c0mpute-cli
  - `c0mpute storage put|get|ls|info|verify|rm|status|tiers|serve`, per
    plugins/storage/module.toml. Commands that need later CIPs (volume, mount,
    provide) are absent rather than stubbed, and --help says which CIP brings
    each one.

Two bugs found by running it, not by the tests:

1. Data loss on rollback. A write that fails its hash commitment used to delete
   every shard hash it touched. Shards are content-addressed and shared, so
   re-uploading an existing object's bytes under a wrong hash produces the same
   shard hashes — the rollback deleted the intact object's shards. One
   malformed request destroyed real data. ChunkStore::put_new now reports
   whether it created a chunk, and rollback only removes what it created.
   Regression tests at both the store and HTTP level.

2. Tracing wrote to stdout, so `HASH=$(c0mpute storage put f)` captured log
   lines and every scripted use broke. Diagnostics now go to stderr; daemon
   mode is unaffected because it points both at the same log file.

Also: Config's api/storage/gateway sections get serde defaults, so a partial
config.toml that sets only [storage] root loads instead of erroring.

65 new tests (31 store, 12 auth, 22 HTTP integration); workspace is green and
clippy-clean on the touched crates. Verified end to end against a running
server: 12 MiB round-trips byte-identical, range reads match dd, and an object
still reconstructs after deleting 4 of 14 shards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsQAuvXkmyHTgnvquLHrRx
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

8 finding(s)

HIGH/CRITICAL: 5 | MEDIUM: 3

Severity Rule Location
HIGH sh-remote-script-execution scripts/dev-setup.sh:25
HIGH sh-remote-script-execution scripts/install.sh:159
HIGH sh-remote-script-execution scripts/install.sh:184
HIGH sh-remote-script-execution scripts/install.sh:277
HIGH sh-remote-script-execution scripts/install.sh:294
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:53
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:89
MEDIUM sh-eval-expansion scripts/dev-setup.sh:35

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 917d001 into feat/storage-cips Aug 29, 2026
5 checks passed
ralyodio added a commit that referenced this pull request Aug 29, 2026
…#24)

Implements CIP-002: turns the c0mpute-store engine into a usable service and
puts it behind a CLI. Single node — every shard lands on the local disk and
host_hint stays None; cross-node placement is CIP-003.

c0mpute-store
  - Tier (hot / standard / critical) carrying (k, parity) and CIP-001 pricing.
    hot is RS k=1, which makes parity shards byte-identical copies: real 3-copy
    replication with 1x repair amplification.
  - Block layer: objects split into blocks, each independently RS-encoded, so
    memory is bounded by block size and a range read touches only the blocks it
    needs. CIP-007's random-access files depend on this.
  - Manifest v2 (version, block_size, tier, blocks[]) with a shim that still
    parses v1.
  - block_size_for() scales 4 MiB -> 256 MiB with object size, keeping a 1 TiB
    object's manifest under a megabyte instead of ~262k blocks.
  - put_stream / read_stream / get_range. put is now a wrapper over put_stream
    so there is one write path.
  - Storage::list() for enumerating what a node holds.

c0mpute-gateway
  - /storage/v1/{objects,shards,manifests,status} with Range support, tier
    selection, idempotent PUT, single-flight per hash, and a disk budget.
  - Commit-then-verify on every write: bytes must hash to the hash the caller
    committed to, or 422.
  - ed25519 signed-request auth (DIP-0007 shape) bound to method + path + body
    hash, with a 5-minute skew window. Writes require it; reads do not, since
    the hash is the capability.

c0mpute-cli
  - `c0mpute storage put|get|ls|info|verify|rm|status|tiers|serve`, per
    plugins/storage/module.toml. Commands that need later CIPs (volume, mount,
    provide) are absent rather than stubbed, and --help says which CIP brings
    each one.

Two bugs found by running it, not by the tests:

1. Data loss on rollback. A write that fails its hash commitment used to delete
   every shard hash it touched. Shards are content-addressed and shared, so
   re-uploading an existing object's bytes under a wrong hash produces the same
   shard hashes — the rollback deleted the intact object's shards. One
   malformed request destroyed real data. ChunkStore::put_new now reports
   whether it created a chunk, and rollback only removes what it created.
   Regression tests at both the store and HTTP level.

2. Tracing wrote to stdout, so `HASH=$(c0mpute storage put f)` captured log
   lines and every scripted use broke. Diagnostics now go to stderr; daemon
   mode is unaffected because it points both at the same log file.

Also: Config's api/storage/gateway sections get serde defaults, so a partial
config.toml that sets only [storage] root loads instead of erroring.

65 new tests (31 store, 12 auth, 22 HTTP integration); workspace is green and
clippy-clean on the touched crates. Verified end to end against a running
server: 12 MiB round-trips byte-identical, range reads match dd, and an object
still reconstructs after deleting 4 of 14 shards.


Claude-Session: https://claude.ai/code/session_01LsQAuvXkmyHTgnvquLHrRx

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant