# The surface

**Contract:** 2026-09-10, the day this surface last changed; served as
`Ulab-Contract` on every response and carried by the SDK as `bnbytes.Contract`.

Nine verbs, two refusals, one name grammar. This file is the contract; the
bindings (see [HTTP.md](HTTP.md)) carry it, they do not extend it. The
operational routes of ulab-standard/OPERATIONS.md (`/livez`, `/healthz`,
`/describe`) are not part of it; the binding document states them.

## Names

```
sha256:<64 lowercase hex digits>
```

The digest of the bytes, prefixed by the algorithm so a future algorithm is
visible rather than silent. Names are compared as strings; there is no
canonicalization beyond the grammar itself. A string that does not match the
grammar is not a name — rejecting it is a protocol error of the binding, not a
refusal of content, so it does not appear in the refusal list.

## Verbs

| verb | in | out |
|---|---|---|
| `get` | name | the bytes |
| `get many` | names | blobs, keyed by name, unordered |
| `has` | names | which of them are here |
| `size` | names | byte count per name that is here |
| `get range` | name, offset, length | the overlapping bytes |
| `get stream` | name | the bytes, incrementally |
| `put` | bytes | name |
| `put many` | list of bytes | names, per item |
| `put stream` | bytes, incrementally | name |

`has` and `size` are **plural**. Singular existence-checking pays one round
trip per node, and the client that asks is usually deciding fetch-whole versus
stream for a set of children it just discovered, not for one blob. A plural
`size` quietly subsumes `has` — a name with a size is here, a name without one
is not — so a binding may carry both verbs on one operation. The singular case
is a set of one.

## Pinned behaviors

These were once open questions ([#3](https://github.com/Bitspark/bn-bytes/issues/3));
they are now contract. None of them changes what the service *is* — which held
only under these answers, and is why they were decided before an implementation
could decide them by accident.

**`put stream` commits atomically, at clean end-of-stream, under the hash of
exactly what arrived.** Integrity under the full content's hash cannot be
violated — the server only knows the hash of what it received, and those bytes
self-verify. What this clause forbids is *prefix-commit*: an aborted stream's
prefix also self-verifies, so committing it under `hash(prefix)` breaks no
naming identity, yet a partial object becomes visible under a hash. Therefore:
any abort — client disconnect, or the server tripping its deployment cap
mid-stream — commits nothing. Dedup at end-of-stream is the ordinary
idempotent put; note that this short-circuit is necessarily **server-side and
commit-time-only**, unlike the `has`-before-`put` skip, which is the SDK's.

**`put many` is per-item, not atomic across the batch.** Batch atomicity would
falsify the README's "a crashed top-down `put many` leaves [a dangling node]",
and the leaves-first write convention exists precisely because batches land
partially. Retries re-put, dedup absorbs them, residue falls under "GC must be
driven by whoever holds the roots." An oversized item refuses alone; its
neighbors land.

**`get range` past the end is a short read.** Refusal would be legal — size is
structural, not semantic — but the refusal enumeration decides it: a range
refusal would grow the list of two. The store returns the overlap of the
requested range with the blob; a range entirely past the end overlaps nothing
and returns zero bytes. A range is the only read verify-on-read cannot check;
immutability makes `size` a reliable clamp, and a client that needs
verification fetches whole or streams.

**`get many` is unordered, keyed by name.** Ordering is most of what the
plural form would otherwise cost; keying is free, since verify-on-read hashes
every blob anyway, and it handles duplicate names and per-item absence — a
name that is not here is simply absent from the result, not an error.

**`get stream` cancellation is nothing, server-side.** The client stops
reading; the server stops sending. No state, no verb, no cleanup protocol.

## Refusals

Only two, neither about content:

| refusal | meaning |
|---|---|
| `too large` | the bytes exceed what this deployment accepts |
| `not found` | the name asked for is not here |

There is no "malformed" — that is a judgement about a format, and there is no
format here to judge against.

`not found` is a refusal only where a single blob's bytes were demanded
(`get`, `get range`, `get stream`). In the plural verbs absence is data —
an omitted key — because a batch that fails on its most absent member is
useless for the negotiation the plural verbs exist to serve.

## Persistence, cancellation and uncertain outcomes

An acknowledged `put`, `put stream` or `put many` item is durable before it is
acknowledged: the bytes are written and flushed to their file, the file is
published under its name by one rename, and the containing directories are
flushed where the platform allows it. The blob survives a process crash and a
restart with the same data directory. Power-loss durability is bounded by the
filesystem and device honouring those flushes; the evidence behind this
paragraph is process-kill and restart tests, not power-loss tests.

Reads never return bytes that were not committed: a name is either absent or
names the complete blob. A crash mid-stream leaves at most a temporary file
that no verb can reach, never a partial object under a name.

A caller that cancels, times out or loses the connection after sending bytes
learns nothing about the outcome: the request either committed the whole blob
or committed nothing, never part of one. Because names are content-derived
and `put` is idempotent, the caller resolves the uncertainty by checking the
expected name with `has` or by sending the same bytes again; both are safe.
An acknowledgement can be lost after a commit, so an error after dispatch is
not proof that the blob is absent. Cancelling a `get stream` is nothing
server-side; cancelling a `put stream` commits nothing.

## What the SDK adds, off the wire

Verify-on-read, cache tiers, skipping a `put` when `has` already says yes,
parallel fetch, failover across mirrors. These live in the client so that the
storage does not have to be trusted — only available. Nothing on the wire
knows about any of them.
