Skip to content

Commit ac2bd3c

Browse files
frristclaude
andcommitted
review(uploader,forgeclient): WithConclude(false) replaces the parked add/upload split
Fold the PR #40 API-shape review (r3674608706, r3674935223): the deferred accept is an option on the one entry point, not a second entry point. - forgeclient: ParkedBlob's fields (AddTask, AcceptTask, PutInvocation) fold into AddedBlob; Location == nil marks an unconcluded add. BlobAdd takes WithConclude (default true); BlobAddParked becomes the private durable half. BlobConclude takes the AddedBlob, no-ops when already concluded, and drops the spent PutInvocation from its result. - uploader: UploadBlob gains UploadOption and returns UploadedBlob (nil-able *BlobLocation + the pending-accept state, populated only while parked); DeferredBodyUploader embeds BodyUploader and keeps ConcludeBlob/AbortBlob. Callers guard the "concluding upload always returns a location" contract explicitly instead of deref-panicking on a misbehaving impl. - s3frontend: parkBlobs passes WithConclude(false); the dedup-vs-parked branch is a documented Location nil-check. The park vocabulary stays in the registry layer (blob_parks, BlobPark, ParkStore) and docs — it is the blob-removal RFC's lifecycle term for the allocated-but-unaccepted state; only the redundant API split is gone. Validated: unit suite; full itest partition green against piri#30 head (fil-forge/piri@b5208a3) + hilt#36 images. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent fe6220a commit ac2bd3c

10 files changed

Lines changed: 242 additions & 168 deletions

File tree

docs/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,7 @@ negotiations).
532532
| Capability | Service | Status | Notes |
533533
|------------------------------------------------------------------------------------------------|---------------------------------|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
534534
| `allocate` / `PUT` / `accept` blob lifecycle | Piri/Sprue | **exists** | The storage primitive Ingot builds on. |
535-
| 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). |
535+
| Ingot-timed accept (PUT a part, defer the conclude until Complete) | Ingot + Sprue | **exists** | `forgeclient.BlobAdd` with `WithConclude(false)` stops at the conclude seam (`BlobConclude` finishes it); 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). |
536536
| `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. |
537537
| `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). |
538538
| 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. |

forgeclient/blobabort.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515

1616
// BlobAbort invokes /blob/abort against the upload service (sprue),
1717
// abandoning the space's in-flight upload of a parked (never-accepted)
18-
// blob. cause is the /blob/add task link (ParkedBlob.AddTask) — sprue
18+
// blob. cause is the /blob/add task link (AddedBlob.AddTask) — sprue
1919
// walks its receipt chain to locate the storage node holding the parked
2020
// bytes (which have no registration or acceptance to look up by) and
2121
// forwards a /blob/reject there. The space is the invocation subject.

forgeclient/blobadd.go

Lines changed: 91 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
// not just the client's token store.
77
// - No /blob/accept re-delegation: sprue owns accept (as it owns
88
// allocate), so the conclude/put-receipt dance carries no space proof.
9-
// - BlobAdd decomposes into BlobAddParked + BlobConclude so multipart can
10-
// defer the conclude (park at UploadPart, accept at Complete).
9+
// - BlobAdd accepts WithConclude(false) so multipart can defer the
10+
// conclude (park at UploadPart, accept at Complete); BlobConclude
11+
// finishes the parked add later.
1112
//
1213
// Also dropped from upstream: otel spans, go-log, ctxutil, the progress/stall
1314
// readers, and the hard requirement that the accept receipt carry a PDP
@@ -57,11 +58,14 @@ type BlobAddConfig struct {
5758
// call instead of the client's default token store — used to scope an
5859
// invocation to a request's per-access-key proofs.
5960
ProofStore ucanlib.ProofStore
61+
// Conclude controls whether BlobAdd concludes the /http/put receipt
62+
// (triggering /blob/accept) before returning. Default true.
63+
Conclude bool
6064
}
6165

6266
// NewBlobAddConfig builds a BlobAddConfig from options.
6367
func NewBlobAddConfig(options ...BlobAddOption) *BlobAddConfig {
64-
cfg := &BlobAddConfig{PutClient: &http.Client{}}
68+
cfg := &BlobAddConfig{PutClient: &http.Client{}, Conclude: true}
6569
for _, opt := range options {
6670
opt(cfg)
6771
}
@@ -88,62 +92,65 @@ func WithProofStore(ps ucanlib.ProofStore) BlobAddOption {
8892
return func(cfg *BlobAddConfig) { cfg.ProofStore = ps }
8993
}
9094

91-
// AddedBlob is the result of a successful BlobAdd.
92-
type AddedBlob struct {
93-
Digest multihash.Multihash
94-
Size uint64
95-
Location ucan.Invocation // the /assert/location commitment
95+
// WithConclude controls whether BlobAdd concludes the upload before
96+
// returning. WithConclude(false) leaves the blob parked — durable on the
97+
// provider, but piri holds the bytes without aggregating them until
98+
// /blob/accept fires: the returned AddedBlob has a nil Location and carries
99+
// the state [Client.BlobConclude] needs to finish the upload later (or
100+
// [Client.BlobAbort] to abandon it; AddTask is the abort Cause). Moot on
101+
// dedup — when the provider already held accepted bytes for the content,
102+
// accept already ran and the add completes regardless.
103+
func WithConclude(conclude bool) BlobAddOption {
104+
return func(cfg *BlobAddConfig) { cfg.Conclude = conclude }
96105
}
97106

98-
// ParkedBlob is the persistable state of a blob that has been added and
99-
// uploaded (durable on the provider) but not yet concluded — piri holds the
100-
// bytes without aggregating them until /blob/accept fires. Persist it and
101-
// finish the upload later with [Client.BlobConclude], or abandon it with
102-
// [Client.BlobAbort] (AddTask is the abort Cause).
103-
type ParkedBlob struct {
107+
// AddedBlob is the result of a BlobAdd. Location is set once the blob is
108+
// accepted; with WithConclude(false) it is nil until the deferred
109+
// [Client.BlobConclude] — persist the task links + PutInvocation in between.
110+
type AddedBlob struct {
104111
Digest multihash.Multihash
105112
Size uint64
113+
// Location is the /assert/location commitment issued at accept; nil
114+
// while the add is unconcluded (parked).
115+
Location ucan.Invocation
106116
// AddTask is the /blob/add task link — the receipt-chain root the
107117
// upload service uses to locate the provider for abort.
108118
AddTask cid.Cid
109119
// AcceptTask is the /blob/accept task link BlobConclude polls.
110120
AcceptTask cid.Cid
111-
// PutInvocation is the sealed /http/put invocation. Its metadata embeds
112-
// the derived signer keys needed to synthesize the put receipt at
113-
// conclude time — treat it as sensitive and delete it once concluded or
114-
// rejected.
121+
// PutInvocation is the issued /http/put invocation, populated only while
122+
// the add is unconcluded. Its metadata embeds the derived signer keys
123+
// needed to synthesize the put receipt at conclude time — treat it as
124+
// sensitive and delete it once concluded or rejected.
115125
PutInvocation []byte
116126
}
117127

118128
// BlobAdd adds a blob to the upload service (sprue): invoke /blob/add,
119129
// PUT the bytes, conclude a synthesized /http/put receipt, then poll the
120130
// /blob/accept receipt for the location commitment. The issuer needs a
121-
// /blob/add delegation proof over space.
122-
//
123-
// It is [Client.BlobAddParked] + [Client.BlobConclude] in one shot.
131+
// /blob/add delegation proof over space. With WithConclude(false) it stops
132+
// after the PUT — the blob stays parked until [Client.BlobConclude].
124133
func (c *Client) BlobAdd(ctx context.Context, space did.DID, content io.Reader, options ...BlobAddOption) (AddedBlob, error) {
125-
parked, added, err := c.BlobAddParked(ctx, space, content, options...)
134+
cfg := NewBlobAddConfig(options...)
135+
added, err := c.blobAdd(ctx, space, content, cfg)
126136
if err != nil {
127137
return AddedBlob{}, err
128138
}
129-
if added != nil {
130-
return *added, nil
139+
// Already accepted (dedup) or deliberately unconcluded — done either way.
140+
if added.Location != nil || !cfg.Conclude {
141+
return added, nil
131142
}
132-
return c.BlobConclude(ctx, space, *parked)
143+
return c.BlobConclude(ctx, space, added)
133144
}
134145

135-
// BlobAddParked runs the durable half of BlobAdd: /blob/add + PUT the bytes,
146+
// blobAdd runs the durable half of BlobAdd: /blob/add + PUT the bytes,
136147
// WITHOUT concluding the /http/put receipt — the conclude is what makes the
137148
// upload service trigger /blob/accept on the provider, so the blob stays
138-
// parked (stored, unaggregated) until BlobConclude.
139-
//
140-
// Exactly one of the returns is non-nil: a ParkedBlob awaiting conclude, or
141-
// — when the provider already held accepted bytes for this content (dedup:
142-
// allocate returned no upload address and the put receipt was pre-issued, so
143-
// accept already ran) — the completed AddedBlob.
144-
func (c *Client) BlobAddParked(ctx context.Context, space did.DID, content io.Reader, options ...BlobAddOption) (parked *ParkedBlob, added *AddedBlob, err error) {
145-
cfg := NewBlobAddConfig(options...)
146-
149+
// parked (stored, unaggregated) until BlobConclude. The result's Location is
150+
// nil unless the provider already held accepted bytes for this content
151+
// (dedup: allocate returned no upload address and the put receipt was
152+
// pre-issued, so accept already ran).
153+
func (c *Client) blobAdd(ctx context.Context, space did.DID, content io.Reader, cfg *BlobAddConfig) (blob AddedBlob, err error) {
147154
putClient := cfg.PutClient
148155
contentReader := content
149156
contentHash := cfg.PrecomputedDigest
@@ -153,20 +160,20 @@ func (c *Client) BlobAddParked(ctx context.Context, space did.DID, content io.Re
153160
start := time.Now()
154161
defer func() {
155162
if err != nil {
156-
c.logger.Error("blob add (parked) failed", zap.Stringer("space", space), zap.Error(err), zap.Duration("duration", time.Since(start)))
163+
c.logger.Error("blob add failed", zap.Stringer("space", space), zap.Error(err), zap.Duration("duration", time.Since(start)))
157164
} else {
158-
c.logger.Debug("blob added (parked)", zap.Stringer("space", space), zap.Duration("duration", time.Since(start)))
165+
c.logger.Debug("blob added", zap.Stringer("space", space), zap.Bool("parked", blob.Location == nil), zap.Duration("duration", time.Since(start)))
159166
}
160167
}()
161168

162169
if needsHash {
163170
contentBytes, rerr := io.ReadAll(content)
164171
if rerr != nil {
165-
return nil, nil, fmt.Errorf("reading content: %w", rerr)
172+
return AddedBlob{}, fmt.Errorf("reading content: %w", rerr)
166173
}
167174
contentHash, err = multihash.Sum(contentBytes, multihash.SHA2_256, -1)
168175
if err != nil {
169-
return nil, nil, fmt.Errorf("computing content multihash: %w", err)
176+
return AddedBlob{}, fmt.Errorf("computing content multihash: %w", err)
170177
}
171178
contentReader = bytes.NewReader(contentBytes)
172179
contentSize := uint64(len(contentBytes))
@@ -180,7 +187,7 @@ func (c *Client) BlobAddParked(ctx context.Context, space did.DID, content io.Re
180187

181188
proofs, proofLinks, err := proofStore.ProofChain(ctx, c.signer.DID(), blobcmds.Add.Command, space)
182189
if err != nil {
183-
return nil, nil, fmt.Errorf("building proof chain: %w", err)
190+
return AddedBlob{}, fmt.Errorf("building proof chain: %w", err)
184191
}
185192

186193
inv, err := blobcmds.Add.Invoke(
@@ -190,59 +197,59 @@ func (c *Client) BlobAddParked(ctx context.Context, space did.DID, content io.Re
190197
invocation.WithProofs(proofLinks...),
191198
)
192199
if err != nil {
193-
return nil, nil, fmt.Errorf("generating invocation: %w", err)
200+
return AddedBlob{}, fmt.Errorf("generating invocation: %w", err)
194201
}
195202

196203
addOK, _, meta, err := Execute[*blobcmds.AddOK](
197204
ctx, c.ucanClient, inv,
198205
execution.WithDelegations(proofs...),
199206
)
200207
if err != nil {
201-
return nil, nil, fmt.Errorf("executing blob add: %w", err)
208+
return AddedBlob{}, fmt.Errorf("executing blob add: %w", err)
202209
}
203210

204211
accInv, err := findInvocation(addOK.Site.Task, meta.Invocations())
205212
if err != nil {
206-
return nil, nil, fmt.Errorf("finding /blob/accept invocation: %w", err)
213+
return AddedBlob{}, fmt.Errorf("finding /blob/accept invocation: %w", err)
207214
}
208215
var accArgs blobcmds.AcceptArguments
209216
if err := accArgs.UnmarshalCBOR(bytes.NewReader(accInv.ArgumentsBytes())); err != nil {
210-
return nil, nil, fmt.Errorf("unmarshaling /blob/accept arguments: %w", err)
217+
return AddedBlob{}, fmt.Errorf("unmarshaling /blob/accept arguments: %w", err)
211218
}
212219

213220
putInv, err := findInvocation(accArgs.Put.Task, meta.Invocations())
214221
if err != nil {
215-
return nil, nil, fmt.Errorf("finding /http/put invocation: %w", err)
222+
return AddedBlob{}, fmt.Errorf("finding /http/put invocation: %w", err)
216223
}
217224
var putArgs httpcmds.PutArguments
218225
if err := putArgs.UnmarshalCBOR(bytes.NewReader(putInv.ArgumentsBytes())); err != nil {
219-
return nil, nil, fmt.Errorf("unmarshaling /http/put arguments: %w", err)
226+
return AddedBlob{}, fmt.Errorf("unmarshaling /http/put arguments: %w", err)
220227
}
221228
putRcpt := maybeFindReceipt(accArgs.Put.Task, meta.Receipts())
222229

223230
allocInv, err := findInvocation(putArgs.Destination.Task, meta.Invocations())
224231
if err != nil {
225-
return nil, nil, fmt.Errorf("finding /blob/allocate invocation: %w", err)
232+
return AddedBlob{}, fmt.Errorf("finding /blob/allocate invocation: %w", err)
226233
}
227234
var allocArgs blobcmds.AllocateArguments
228235
if err := allocArgs.UnmarshalCBOR(bytes.NewReader(allocInv.ArgumentsBytes())); err != nil {
229-
return nil, nil, fmt.Errorf("unmarshaling /blob/allocate arguments: %w", err)
236+
return AddedBlob{}, fmt.Errorf("unmarshaling /blob/allocate arguments: %w", err)
230237
}
231238
allocRcpt, err := findReceipt(putArgs.Destination.Task, meta.Receipts())
232239
if err != nil {
233-
return nil, nil, fmt.Errorf("finding /blob/allocate receipt: %w", err)
240+
return AddedBlob{}, fmt.Errorf("finding /blob/allocate receipt: %w", err)
234241
}
235242
o, x := allocRcpt.Out().Unpack()
236243
if allocRcpt.Out().IsErr() {
237244
var model edm.ErrorModel
238245
if err := model.UnmarshalCBOR(bytes.NewReader(x)); err != nil {
239-
return nil, nil, fmt.Errorf("executing invocation")
246+
return AddedBlob{}, fmt.Errorf("executing invocation")
240247
}
241-
return nil, nil, fmt.Errorf("failure in allocation receipt: %w", model)
248+
return AddedBlob{}, fmt.Errorf("failure in allocation receipt: %w", model)
242249
}
243250
var allocOK blobcmds.AllocateOK
244251
if err := allocOK.UnmarshalCBOR(bytes.NewReader(o)); err != nil {
245-
return nil, nil, fmt.Errorf("unmarshaling allocation receipt output: %w", err)
252+
return AddedBlob{}, fmt.Errorf("unmarshaling allocation receipt output: %w", err)
246253
}
247254

248255
putSuccess := putRcpt != nil && putRcpt.Out().IsOK()
@@ -253,7 +260,7 @@ func (c *Client) BlobAddParked(ctx context.Context, space did.DID, content io.Re
253260
// piri's PUT endpoint requires it.
254261
if allocOK.Address != nil && !putSuccess {
255262
if err := putBlob(ctx, putClient, allocOK.Address.URL.URL(), allocOK.Address.Headers, contentReader, int64(*contentSizePtr)); err != nil {
256-
return nil, nil, fmt.Errorf("putting blob: %w", err)
263+
return AddedBlob{}, fmt.Errorf("putting blob: %w", err)
257264
}
258265
}
259266

@@ -264,29 +271,39 @@ func (c *Client) BlobAddParked(ctx context.Context, space did.DID, content io.Re
264271
if putSuccess {
265272
location, aerr := c.awaitAccept(ctx, accInv.Task().Link())
266273
if aerr != nil {
267-
err = aerr
268-
return nil, nil, err
274+
return AddedBlob{}, aerr
269275
}
270-
return nil, &AddedBlob{Digest: contentHash, Size: *contentSizePtr, Location: location}, nil
276+
return AddedBlob{
277+
Digest: contentHash,
278+
Size: *contentSizePtr,
279+
Location: location,
280+
AddTask: inv.Task().Link(),
281+
AcceptTask: accInv.Task().Link(),
282+
}, nil
271283
}
272284

273285
// Parked: durable on the provider, conclude deferred to BlobConclude.
274-
return &ParkedBlob{
286+
return AddedBlob{
275287
Digest: contentHash,
276288
Size: *contentSizePtr,
277289
AddTask: inv.Task().Link(),
278290
AcceptTask: accInv.Task().Link(),
279291
PutInvocation: putInv.Bytes(),
280-
}, nil, nil
292+
}, nil
281293
}
282294

283-
// BlobConclude finishes a parked upload: it synthesizes and concludes the
284-
// /http/put receipt (which makes the upload service trigger /blob/accept on
285-
// the provider) and awaits the accept receipt's location commitment. Accept
286-
// is owned by sprue (like allocate), so the conclude carries no space proof.
287-
// Safe to retry — re-concluding an already-concluded put is tolerated
288-
// upstream.
289-
func (c *Client) BlobConclude(ctx context.Context, space did.DID, parked ParkedBlob) (blob AddedBlob, err error) {
295+
// BlobConclude finishes a parked (unconcluded) BlobAdd: it synthesizes and
296+
// concludes the /http/put receipt (which makes the upload service trigger
297+
// /blob/accept on the provider) and awaits the accept receipt's location
298+
// commitment. Accept is owned by sprue (like allocate), so the conclude
299+
// carries no space proof. Safe to retry — re-concluding an already-concluded
300+
// put is tolerated upstream, and an AddedBlob whose Location is already set
301+
// returns as-is. The result drops PutInvocation (spent — the caller should
302+
// delete its persisted copy too).
303+
func (c *Client) BlobConclude(ctx context.Context, space did.DID, added AddedBlob) (blob AddedBlob, err error) {
304+
if added.Location != nil {
305+
return added, nil
306+
}
290307
start := time.Now()
291308
defer func() {
292309
if err != nil {
@@ -297,19 +314,25 @@ func (c *Client) BlobConclude(ctx context.Context, space did.DID, parked ParkedB
297314
}()
298315

299316
putInv := new(invocation.Invocation)
300-
if err := putInv.UnmarshalCBOR(bytes.NewReader(parked.PutInvocation)); err != nil {
317+
if err := putInv.UnmarshalCBOR(bytes.NewReader(added.PutInvocation)); err != nil {
301318
return AddedBlob{}, fmt.Errorf("decoding parked /http/put invocation: %w", err)
302319
}
303320

304321
if err := c.sendPutReceipt(ctx, putInv); err != nil {
305322
return AddedBlob{}, fmt.Errorf("sending put receipt: %w", err)
306323
}
307324

308-
location, err := c.awaitAccept(ctx, parked.AcceptTask)
325+
location, err := c.awaitAccept(ctx, added.AcceptTask)
309326
if err != nil {
310327
return AddedBlob{}, err
311328
}
312-
return AddedBlob{Digest: parked.Digest, Size: parked.Size, Location: location}, nil
329+
return AddedBlob{
330+
Digest: added.Digest,
331+
Size: added.Size,
332+
Location: location,
333+
AddTask: added.AddTask,
334+
AcceptTask: added.AcceptTask,
335+
}, nil
313336
}
314337

315338
// awaitAccept polls the /blob/accept receipt and extracts the

0 commit comments

Comments
 (0)