Skip to content

Commit 43d6482

Browse files
frristclaude
andcommitted
feat(blob)!: RFC-shaped remove/abort args; recognize BlobAccepted refusals
The blob-removal RFC drops the space from the client-leg arguments — /blob/remove is {digest} and /blob/abort is {digest, cause}, with the space as the invocation subject. Ingot already invoked with the space as subject, so only the argument literals change. Treat the node's BlobAccepted refusal on abort as final rather than a fault: it means this space accepted the same content concurrently, so the blob belongs to the reference index and is released via /blob/remove when its last claim drops. The uploader logs it distinctly instead of as an error; the multipart cleanup path already discards the park row, which is obsolete either way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 086a2a8 commit 43d6482

11 files changed

Lines changed: 72 additions & 49 deletions

File tree

docs/architecture.md

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,11 @@ The design is shaped by how the Forge upload pipeline works and by a handful of
9090
- **The 256 MiB blob ceiling is a knob, not a wall.** Piri currently caps a blob at 256 MiB because
9191
it builds the commP Merkle tree in RAM; known improvements (streaming commP) lift this. Ingot
9292
treats it as a tunable maximum and splits larger objects.
93-
- **The delete primitives are built.** `blob/remove` is handled end-to-end (Sprue deregisters +
94-
forwards; Piri releases the space's claim and defers physical deletion until the aggregate root
95-
retires on-chain via the signed `schedulePieceDeletions` path), and `blob/unallocate` retires a
96-
parked (never-accepted) blob — an upload ends in exactly one of accept or unallocate. The
93+
- **The delete primitives are built.** `/blob/remove` is handled end-to-end (Sprue forwards
94+
`/blob/release` to the nodes and deregisters; Piri releases the space's claim and defers physical
95+
deletion until the aggregate root retires on-chain via the signed `schedulePieceDeletions`
96+
path), and `/blob/abort` — translated by Sprue into `/blob/reject` on the node — retires a
97+
parked (never-accepted) blob — an upload ends in exactly one of accept or reject. The
9798
indexer still has no retraction. [§9](#9-the-system-contract-piri--sprue--indexer) enumerates
9899
the contract Ingot needs.
99100

@@ -355,7 +356,7 @@ fixed constant (pdp-sim).
355356
discarded). This is the sub-`min` tail.
356357
357358
**The delete primitives** are deliberately distinct:
358-
- **`unallocate(digest)`** retires a **parked** blob (PUT, never accepted): delete the MinIO bytes
359+
- **`abort(digest)`** retires a **parked** blob (PUT, never accepted): delete the MinIO bytes
359360
and the allocation record. No chain involvement.
360361
- **`remove(digest)`** releases a **space's claim** on an **accepted** blob. Because dedup is global,
361362
Piri deletes the bytes and retires the piece only when the per-`(digest, space)` claim count
@@ -428,7 +429,7 @@ CompleteMultipartUpload([parts])
428429
splice; blob_refs += version per digest; guarded root swap
429430
AbortMultipartUpload
430431
latch session open→aborting
431-
parked blobs → unallocate; already-accepted (deduped) blobs → remove (§6)
432+
parked blobs → abort; already-accepted (deduped) blobs → remove (§6)
432433
```
433434
434435
A completed multipart object is the ordered union of its parts' blobs plus a manifest of byte ranges;
@@ -439,13 +440,13 @@ later `GET`/`HEAD ?partNumber=N` can resolve part `N` to its byte span and repor
439440
### 7.3 The session latch (the Abort/Complete race)
440441
441442
`Complete` and `Abort` can arrive concurrently for one `uploadId`. Without coordination they collide
442-
on the parts — `Complete` triggering accept while `Abort` unallocates those same parts — leaving the
443+
on the parts — `Complete` triggering accept while `Abort` rejects those same parts — leaving the
443444
object half-built or half-deleted. A **single-winner latch** prevents it: an atomic state transition
444445
on the session row (`UPDATE … SET state=? WHERE state='open'`). Exactly one of
445446
`Complete`→`completing` or `Abort`→`aborting` wins; the loser observes the moved row and returns an
446-
error. Accept is triggered only after the session is latched `completing`, so accept and unallocate
447+
error. Accept is triggered only after the session is latched `completing`, so accept and reject
447448
never touch the same part concurrently. A deduped part is already accepted (not parked), so Abort
448-
removes it via the reference path rather than unallocating it.
449+
removes it via the reference path rather than rejecting it.
449450
450451
### 7.4 Read (`GetObject`)
451452
@@ -471,7 +472,7 @@ updates are transactional with the commit.
471472
472473
| Case | Handling |
473474
|---|---|
474-
| Crash after PUT, before accept | Bytes parked; `upload_intents` (parked) drives resume or `unallocate`. No `200` was sent. |
475+
| Crash after PUT, before accept | Bytes parked; `upload_intents` (parked) drives resume or `abort`. No `200` was sent. |
475476
| Crash after accept, before commit | Blob durable but unreferenced; `upload_intents` (accepted) drives commit-retry or `remove`. |
476477
| Guarded-root-swap mismatch | Reload root, re-splice; blobs already durable, never re-uploaded. |
477478
| Concurrent PUT, same key | Distinct versionIds; serialize only on the swap. |
@@ -525,8 +526,8 @@ negotiations).
525526
|------------------------------------------------------------------------------------------------|---------------------------------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
526527
| `allocate` / `PUT` / `accept` blob lifecycle | Piri/Sprue | **exists** | The storage primitive Ingot builds on. |
527528
| Ingot-timed accept (PUT a part, defer the conclude until Complete) | Ingot + Sprue | **exists** | `forgeclient.BlobAddParked`/`BlobConclude` split the flow at the conclude seam; UploadPart parks (durable, unaggregated), Complete concludes — Sprue's conclude handler already ran accept standalone. Ingot still does **not** issue `accept` (Piri requires the upload-service DID). |
528-
| `unallocate(digest)` — drop a parked blob | Piri + Sprue + libforge | **exists** | `/blob/unallocate` on Piri refuses accepted blobs (`BlobAccepted`), deletes the space's allocation, and drops the bytes at zero allocations. Sprue recovers the provider from the `cause` receipt chain (a parked blob has no registration). Abort/TTL/supersede unwind through it. |
529-
| `remove(digest)` — per-space claim release; physical delete/piece-retire at zero global claims | Piri + Sprue + libforge | **exists** | `/blob/remove` on Piri deletes the space's allocation/acceptance/claim; at zero claims bytes delete immediately (unaggregated) or via the pending-removal sweep once the whole aggregate root is dead (FIL-623/624). Sprue forwards to primary + replicas (FIL-522). |
529+
| `abort(digest)` — drop a parked blob | Piri + Sprue + libforge | **exists** | `/blob/abort` on Sprue translates to `/blob/reject` on Piri, which refuses blobs the invoking space has accepted (`BlobAccepted`), deletes the space's allocation, and drops the bytes once no space holds an allocation or acceptance. Sprue recovers the provider from the `cause` receipt chain (a parked blob has no registration). Abort/TTL/supersede unwind through it. |
530+
| `remove(digest)` — per-space claim release; physical delete/piece-retire at zero global claims | Piri + Sprue + libforge | **exists** | `/blob/remove` on Sprue forwards `/blob/release` to Piri, which deletes the space's allocation/acceptance/claim; at zero claims bytes delete immediately (unaggregated) or via the pending-removal sweep once the whole aggregate root is dead (FIL-623/624). Sprue forwards to primary + replicas (FIL-522). |
530531
| Configurable, adaptive size policy (`min`/`max`); batch guard for the `extraData` cap | Piri | **partial** | `MinAggregateSize` is hardcoded 128 MiB; lower to ~8 MiB and make configurable. The `addPieces` batch is no longer contract-capped (FWSS v1.3.0 removed the `extraData` cap); size it to the FVM `PiecesAdded` event-size + per-tx gas — a measured ceiling (default `BatchSize=10` is safely within it) (pdp-sim). No contract change. |
531532
| Compaction (Regime B) + complete the on-chain delete signature | Piri | **partial** | Whole-root delete is signed and wired (`schedulePieceDeletions` with `SignSchedulePieceRemovals` extraData); compaction (remove + re-hash survivors + re-add) is new. |
532533
| De-dup at accept (don't re-aggregate a digest already a live piece) | Piri | **to-build** | Backstops one-piece-per-content once accept timing is Ingot-driven. |
@@ -539,7 +540,7 @@ negotiations).
539540
540541
**Determinism / idempotency Ingot must preserve.** The `accept` invocation is built deterministically
541542
(stable CID, today via `WithNoNonce` over `{space, digest, size, put-task}`); re-driving accept must
542-
reuse the same put-task link. `remove`/`unallocate` must be idempotent. The forge-root advance must
543+
reuse the same put-task link. `remove`/`abort` must be idempotent. The forge-root advance must
543544
happen only after a successful guarded root swap — the catalog log currently advances it before the
544545
swap, so a mismatch can leave the forge root pointing at a bucket root that was never adopted.
545546
@@ -592,7 +593,7 @@ The MVP this supersedes had six structural problems; each is resolved by a layer
592593
|-----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
593594
| A per-bucket lock held across the whole write | Ingest and upload run off-lock; only the MST splice + guarded root swap is in the critical section. [§7.1](#71-write-single-shot-putobject) |
594595
| The whole object buffered in memory | Hash-while-writing to the local store; stream to Piri; nothing held whole in RAM. [§5](#5-the-data-layer), [§7.1](#71-write-single-shot-putobject) |
595-
| No multipart, no abort | Parked part-blobs, accept-at-Complete, single-winner latch; abort = `unallocate` (parked) / `remove` (deduped). [§7.2](#72-multipart)–7.3, [§9](#9-the-system-contract-piri--sprue--indexer) |
596+
| No multipart, no abort | Parked part-blobs, accept-at-Complete, single-winner latch; abort = `/blob/abort` (parked) / `remove` (deduped). [§7.2](#72-multipart)–7.3, [§9](#9-the-system-contract-piri--sprue--indexer) |
596597
| Objects chunked into CARs to obtain a digest | Object body = one blob (≤ `max`) or a coarse `≤ max` split; no fine chunking, no CAR. [§5](#5-the-data-layer), [§10](#10-deployment-topology--the-digest-before-upload-cost) |
597598
| No delete of superseded data | Reference index + per-space `remove` + Piri's global claim gate + indexer delete; O(1) for`min` blobs. [§5](#5-the-data-layer), [§6](#6-the-forgechain-layer), [§9](#9-the-system-contract-piri--sprue--indexer) |
598599
| Aggregation blocked partial deletes | A small `min` makes most blobs their own piece (O(1) delete); compaction handles only the sub-`min` tail. [§6](#6-the-forgechain-layer) |
@@ -801,20 +802,20 @@ reference index — and is out of scope for this iteration.
801802
The in-memory harness uses a no-op uploader and serves reads from the spool, so these forge-network
802803
paths are stubbed in-tree and verified against a real sprue+piri+indexer later:
803804
804-
- **`remove(digest)` and `unallocate(digest)` are live.** `RemoveBlob` invokes `/blob/remove` on
805-
sprue, which forwards to the storage nodes ([§9](#9-the-system-contract-piri--sprue--indexer)); delete finality means claim-release-now,
806-
bytes-at-root-death. `UnallocateBlob` retires parked part-blobs on abort via `/blob/unallocate`
807-
(provider recovered from the `cause` receipt chain); allocation-expiry GC (FIL-625) remains the
808-
backstop when an abort never arrives.
805+
- **`remove(digest)` and `abort(digest)` are live.** `RemoveBlob` invokes `/blob/remove` on
806+
sprue, which forwards `/blob/release` to the storage nodes ([§9](#9-the-system-contract-piri--sprue--indexer)); delete finality means claim-release-now,
807+
bytes-at-root-death. `AbortBlob` retires parked part-blobs via `/blob/abort` — sprue translates
808+
it into `/blob/reject` on the node (provider recovered from the `cause` receipt chain);
809+
allocation-expiry GC (FIL-625) remains the backstop when an abort never arrives.
809810
- **The local-table `Locator` read tier is not wired.** Body-blob *locations* are recorded at accept,
810811
but the read path that consumes them ([§7.4](#74-read-getobject), [§8](#8-retrieval-addressing-when-bodies-need-a-sharded-dag-index)) is deferred — it is only exercised after spool
811812
eviction (also not built) and is best validated live.
812813
- **Multipart parts park at UploadPart, accept at Complete.** (Built: `parkBlobs`/`concludeBlobs`
813814
over the `blob_parks` table.) The in-process harness still spools parts
814815
at `UploadPart` and uploads+accepts them at `Complete`; the true forge *parking* (upload early,
815-
accept-at-Complete) and `unallocate`-on-abort from [§7.2](#72-multipart)–[7.3](#73-the-session-latch-the-abortcomplete-race) are forge-mode refinements.
816+
accept-at-Complete) and the `/blob/abort` unwind from [§7.2](#72-multipart)–[7.3](#73-the-session-latch-the-abortcomplete-race) are forge-mode refinements.
816817
- **Crash recovery for the spool is not built.** The `upload_intents` × `blob_refs` reconciliation
817-
the failure-mode table in [§7.5](#75-concurrency-durability-and-failure-modes) describes (resume/`unallocate` parked, `remove` accepted-but-unreferenced)
818+
the failure-mode table in [§7.5](#75-concurrency-durability-and-failure-modes) describes (resume/`abort` parked, `remove` accepted-but-unreferenced)
818819
is a later phase; a partial post-commit reference-index write currently relies on retry/idempotency.
819820
- **`UploadPartCopy` and indexer retraction on delete** are unimplemented
820821
(`ErrNotImplemented` / no-op). `ListParts` and `ListMultipartUploads` are implemented
@@ -824,7 +825,7 @@ paths are stubbed in-tree and verified against a real sprue+piri+indexer later:
824825
sessions and committed objects), and a background sweeper aborts open sessions older
825826
than `multipart_session_ttl` (default 7d) and reaps terminal session rows. A successful
826827
Complete retains its session in state `completed` so a duplicate Complete is idempotent
827-
per S3. The network-side `unallocate`-on-abort remains a parking-flow concern (above).
828+
per S3. The network-side `/blob/abort` unwind remains a parking-flow concern (above).
828829
829830
### Known correctness boundary
830831

forgeclient/blobabort.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414

1515
// BlobAbort invokes /blob/abort against the upload service (sprue),
1616
// abandoning the space's in-flight upload of a parked (never-accepted)
17-
// blob. cause is the /space/blob/add task link (ParkedBlob.AddTask) — sprue
17+
// blob. cause is the /blob/add task link (ParkedBlob.AddTask) — sprue
1818
// walks its receipt chain to locate the storage node holding the parked
1919
// bytes (which have no registration or acceptance to look up by) and
2020
// forwards a /blob/reject there. Idempotent on the node; a blob that has
@@ -28,7 +28,9 @@ func (c *Client) BlobAbort(ctx context.Context, digest multihash.Multihash, spac
2828
inv, err := blobcmds.Abort.Invoke(
2929
c.signer,
3030
space,
31-
&blobcmds.AbortArguments{Space: space, Digest: digest, Cause: cause},
31+
// The space is the invocation subject; it is not repeated in the
32+
// arguments.
33+
&blobcmds.AbortArguments{Digest: digest, Cause: cause},
3234
invocation.WithAudience(c.serviceID),
3335
invocation.WithProofs(proofLinks...),
3436
)

forgeclient/blobadd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ type AddedBlob struct {
8888
type ParkedBlob struct {
8989
Digest multihash.Multihash
9090
Size uint64
91-
// AddTask is the /space/blob/add task link — the receipt-chain root the
91+
// AddTask is the /blob/add task link — the receipt-chain root the
9292
// upload service uses to locate the provider for abort.
9393
AddTask cid.Cid
9494
// AcceptTask is the /blob/accept task link BlobConclude polls.

forgeclient/blobremove.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ func (c *Client) BlobRemove(ctx context.Context, digest multihash.Multihash, spa
2424
inv, err := blobcmds.Remove.Invoke(
2525
c.signer,
2626
space,
27-
&blobcmds.RemoveArguments{Space: space, Digest: digest},
27+
// The space is the invocation subject; it is not repeated in the
28+
// arguments.
29+
&blobcmds.RemoveArguments{Digest: digest},
2830
invocation.WithAudience(c.serviceID),
2931
invocation.WithProofs(proofLinks...),
3032
)

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ require (
88
github.com/aws/aws-sdk-go-v2/credentials v1.19.15
99
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.1
1010
github.com/fil-forge/indexing-service v1.13.5-0.20260619142411-efe3f5fab717
11-
github.com/fil-forge/libforge v0.0.0-20260717174550-aac837a730c2
11+
github.com/fil-forge/libforge v0.0.0-20260723212548-3e5e6ba95711
1212
github.com/fil-forge/smelt v0.0.0-20260717195554-271831781c1d
1313
github.com/fil-forge/ucantone v0.0.0-20260706102443-79141c5cc52e
1414
github.com/fxamacker/cbor/v2 v2.9.2

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,8 @@ github.com/fil-forge/go-ucanto v0.0.0-20260507172450-5cb5d073f8ab h1:2J2cDThqTKP
248248
github.com/fil-forge/go-ucanto v0.0.0-20260507172450-5cb5d073f8ab/go.mod h1:lZF3UXZ2hGLKYmXdquG50JqI9pRlUrV6lubGtgOYfwc=
249249
github.com/fil-forge/indexing-service v1.13.5-0.20260619142411-efe3f5fab717 h1:Wke8qgaDgy7DGaIS28VpHip+YHSdQP1hGNEZTrXXzb4=
250250
github.com/fil-forge/indexing-service v1.13.5-0.20260619142411-efe3f5fab717/go.mod h1:wFcakLohOqpMRkJzWRdGFGHRFpGB+PpEMCm9wkt2cqU=
251-
github.com/fil-forge/libforge v0.0.0-20260717174550-aac837a730c2 h1:mwb14/aanwFBGdQJ/0WCkNX/AzUXF/iRKMVjhXtYd6Q=
252-
github.com/fil-forge/libforge v0.0.0-20260717174550-aac837a730c2/go.mod h1:0kXihIQ4L2uZ00nR5XrZ/Y8Db7Ht/qQNuiWslwMJ95M=
251+
github.com/fil-forge/libforge v0.0.0-20260723212548-3e5e6ba95711 h1:xO5gwfL3W2wqaLvwFUDj4MuWX9wHK9TGV8F3jFe/mZM=
252+
github.com/fil-forge/libforge v0.0.0-20260723212548-3e5e6ba95711/go.mod h1:0kXihIQ4L2uZ00nR5XrZ/Y8Db7Ht/qQNuiWslwMJ95M=
253253
github.com/fil-forge/smelt v0.0.0-20260717195554-271831781c1d h1:hRW4J1J1qZVKl+IBMIPwyHkjztoE37wAlBGjjJLkGgk=
254254
github.com/fil-forge/smelt v0.0.0-20260717195554-271831781c1d/go.mod h1:3UpZd5C7w59IDICASaYqIF6Uu/0uZkp7tj9Ivg0ZmRU=
255255
github.com/fil-forge/ucantone v0.0.0-20260706102443-79141c5cc52e h1:di/SseJVEO6bznSH/UEnAE2nSwwp/gqgnPLgE9/x2Zg=

0 commit comments

Comments
 (0)