proven on the hosted testnet instance · 2026-09-13 · QA-14
Store, read back, and know when it is certified
What you will have at the end
Section titled “What you will have at the end”A bucket that belongs to your tenant. An object stored in it and read back with
the same bytes you sent. A HEAD that tells you where that object stands — and,
once it is certified, names the blob it was certified as. Then the object gone,
with a 404 where it was.
You will also know how to ask “is it certified yet?” without guessing, which is the only part of this that is not plain S3.
Prerequisites
Section titled “Prerequisites”- A credential — a key from an invite or a token from your own issuer.
curl, an empty working directory, and the instance up (The testnet instance).HOST,AUTHandBUCKETset. That is the first block of the Quickstart; source it before the blocks below.
1. Create a bucket
Section titled “1. Create a bucket”A bucket is private to the tenant that created it. Twelve operations check that ownership, so another tenant asking to write, read, list or delete inside yours is refused — see Tenancy.
# Create the bucket. 200 means it exists and it is yours.
curl -sS -m 60 -X PUT -H "$AUTH" \ -o /dev/null -w 'bucket %{http_code}\n' "$HOST/$BUCKET"200 means it exists and it is yours.
2. Store an object
Section titled “2. Store an object”An ordinary S3 PUT. Nothing about the request asks for encryption, provenance
or certification — all three happen on the way in.
# Store a local file as report.bin. Bring your own bytes, or make 64 KiB.
[ -f local.bin ] || head -c 65536 /dev/urandom > local.bin
curl -sS -m 120 -X PUT -H "$AUTH" --data-binary @local.bin \ -o /dev/null -w 'put %{http_code}\n' "$HOST/$BUCKET/report.bin"The bytes are encrypted at rest, per tenant, by default. You never see that on
this path: what you PUT is what you GET. The only way to opt out is an
explicit header, which is Encryption at
rest.
3. Read it back and compare
Section titled “3. Read it back and compare”The assertion that matters. Not “it answered 200” — the same bytes.
# Read it back and compare it to what you sent.
curl -sS -m 120 -H "$AUTH" -o roundtrip.bin "$HOST/$BUCKET/report.bin"
cmp local.bin roundtrip.bin && echo byte-equalcmp is silent and the line reads byte-equal, or something went wrong and you
want to know now rather than after a thousand objects.
4. Ask where the object stands
Section titled “4. Ask where the object stands”HEAD carries two headers of Permafrost’s own. This block prints them, then
polls until the second one appears.
# HEAD carries Permafrost's own metadata. The sync state is always there. The# blob id appears only once the object is certified, so its absence is# "not yet certified", not an error.
HEADERS=$(curl -sS -m 60 -I -H "$AUTH" "$HOST/$BUCKET/report.bin")printf '%s\n' "$HEADERS" | grep -i '^x-amz-meta-permafrost-'
WAIT_SECS=${BLOBID_WAIT_SECS:-300}echo "waiting up to ${WAIT_SECS}s for the blob id"
WAITED=0BLOB_ID=""while [ "$WAITED" -lt "$WAIT_SECS" ]; do HEADERS=$(curl -sS -m 60 -I -H "$AUTH" "$HOST/$BUCKET/report.bin") BLOB_ID=$(printf '%s\n' "$HEADERS" \ | grep -i '^x-amz-meta-permafrost-blob-id:' | cut -d' ' -f2- | tr -d '\r' || true) if [ -n "$BLOB_ID" ]; then break; fi sleep 5 WAITED=$((WAITED + 5))done
echo "blob id: ${BLOB_ID:-not yet certified} (after ${WAITED}s)"x-amz-meta-permafrost-sync-state is always there. It is the object’s
position in its own lifecycle: PENDING, UPLOADING, SYNCED, FAILED or
DELETED. A fresh object is not SYNCED yet, and that is the normal state for a
moment after a write.
x-amz-meta-permafrost-blob-id is there only once the object is certified.
It appears when the row has reached SYNCED and carries a blob id — both
conditions, so you are never handed a locator that does not resolve. Its
absence is the “not yet” signal, not an error. Poll HEAD until it shows up,
which is exactly what the loop above does.
That is the whole protocol for “has it landed”: ask HEAD again. There is no
callback, no queue to watch and no separate status route to learn.
Once it has landed, the object also has a receipt on Sui. Finding it is Read your receipt on Sui.
5. Delete it
Section titled “5. Delete it”# Delete it. Expect 204, then 404 on the read path.
curl -sS -m 60 -X DELETE -H "$AUTH" \ -o /dev/null -w 'delete %{http_code}\n' "$HOST/$BUCKET/report.bin"
curl -sS -m 60 -H "$AUTH" \ -o /dev/null -w 'get after delete %{http_code}\n' "$HOST/$BUCKET/report.bin"204 on the delete, then 404 on the read path. Those two lines are the check.
The deleted object also drops out of the keyless verify answer: asking Verify a file by hash for its hash no longer returns provenance, unless some other object you still hold has the same bytes.
One thing that does not apply here
Section titled “One thing that does not apply here”Neither of those two headers is emitted for a multipart object. A multipart
object’s bytes resolve through its upload’s manifest and part rows rather than
through a single blob, so a HEAD on one carries no sync state and no blob id —
not “not yet”, but never. If you are uploading media in parts, read Upload media
in parts, read by range instead, and do not
build a wait loop around headers that will not arrive.
What to read next
Section titled “What to read next”- The S3 surface — which verbs are supported, and what is not implemented.
- Headers, status codes, 401 and 429 — the exact header values, and what a refusal means.
- Limits — the ceilings a tenant runs inside.
Permafrost runs on Sui testnet and Walrus testnet. Everything here describes a shipped testnet instance, not a production service.