From c715153931e4ca463e586541ebf5ad5d4d6bba6f Mon Sep 17 00:00:00 2001 From: frrist Date: Wed, 22 Jul 2026 16:35:14 -0700 Subject: [PATCH 1/2] feat!: align removal capabilities with the blob-removal RFC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split the provider leg out of /blob/remove into a new /blob/release binding: /blob/remove and /blob/abort are now space-subject client verbs whose arguments no longer carry the space (it is the invocation subject), while /blob/release carries {space, digest} under the provider subject, matching /blob/allocate and /blob/accept. Define the RFC's named errors where both services can share them: BlobAccepted (reject refused because the invoking space accepted the blob — guard is space-scoped, not digest-scoped) and MissingCause (abort's cause is missing or unknown). Co-Authored-By: Claude Fable 5 --- commands/blob/abort.go | 28 +++++-- commands/blob/abort_test.go | 16 +++- commands/blob/cbor_gen.go | 147 +++++++++++++++++++++++++++------- commands/blob/gen/main.go | 1 + commands/blob/json_gen.go | 154 +++++++++++++++++++++++++++--------- commands/blob/reject.go | 27 +++++-- commands/blob/release.go | 28 +++++++ commands/blob/remove.go | 20 ++--- commands/blob/types.go | 36 ++++++--- 9 files changed, 352 insertions(+), 105 deletions(-) create mode 100644 commands/blob/release.go diff --git a/commands/blob/abort.go b/commands/blob/abort.go index 38bff59..21560f3 100644 --- a/commands/blob/abort.go +++ b/commands/blob/abort.go @@ -5,22 +5,36 @@ package blob import ( "github.com/fil-forge/libforge/commands" "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/errors" "github.com/fil-forge/ucantone/ucan/command" ) type AbortOK = commands.Unit -// Abort (/blob/abort) abandons an in-flight upload of a PARKED blob — one -// that was allocated and uploaded (HTTP PUT) but never accepted. It is the -// client-facing abandon verb: an upload ends in exactly one of -// `/blob/accept` (commit) or an abort that the upload service translates -// into `/blob/reject` on the storage node holding the blob. +// Abort (/blob/abort) abandons an in-flight upload of a PARKED blob — +// allocated but never accepted, whether or not the bytes ever reached the +// storage node. It is the client-facing abandon verb: an upload ends in +// exactly one of `/blob/accept` (commit) or an abort that the upload service +// translates into `/blob/reject` on the storage node holding the allocation. // // Served by the upload service (subject = the space). A parked blob has no // registration or acceptance to look the storage node up by, so the service -// recovers it from the Cause receipt chain and forwards a `/blob/reject`. -// Blobs with an acceptance are released via `/blob/remove` instead. +// recovers it from the Cause receipt chain and forwards a `/blob/reject` +// (Cause itself is not forwarded — it is routing metadata, meaningless to +// the node). A missing or unknown Cause fails with MissingCause. Blobs the +// space has accepted are released via `/blob/remove` instead; if the node +// refuses the translated reject with BlobAccepted, the service surfaces that +// named failure in the abort receipt. The abort mutates no upload-service +// state, so a failed abort is safely retryable. // // Idempotent: aborting an unknown or already-rejected blob succeeds. // The receipt carries no payload (Unit). var Abort = binding.Bind[*AbortArguments, *AbortOK](command.MustParse("/blob/abort")) + +// MissingCauseErrorName is the stable receipt-failure name when an abort's +// Cause is missing or does not resolve to a known `/space/blob/add` task — +// without it the upload service cannot recover which storage node holds the +// parked blob. +const MissingCauseErrorName = "MissingCause" + +var ErrMissingCause = errors.New(MissingCauseErrorName, "abort requires the cause of the /space/blob/add task that parked the blob") diff --git a/commands/blob/abort_test.go b/commands/blob/abort_test.go index 133e6dd..5cbba06 100644 --- a/commands/blob/abort_test.go +++ b/commands/blob/abort_test.go @@ -14,7 +14,6 @@ import ( // Round-trips AbortArguments through cbor. func TestAbortArgumentsRoundTrip(t *testing.T) { in := blob.AbortArguments{ - Space: testutil.RandomDID(t), Digest: testutil.RandomMultihash(t), Cause: testutil.RandomCID(t), } @@ -22,11 +21,24 @@ func TestAbortArgumentsRoundTrip(t *testing.T) { require.NoError(t, in.MarshalCBOR(&buf)) var out blob.AbortArguments require.NoError(t, out.UnmarshalCBOR(&buf)) - require.Equal(t, in.Space, out.Space) require.Equal(t, in.Digest, out.Digest) require.Equal(t, in.Cause, out.Cause) } +// Round-trips ReleaseArguments through cbor. +func TestReleaseArgumentsRoundTrip(t *testing.T) { + in := blob.ReleaseArguments{ + Space: testutil.RandomDID(t), + Digest: testutil.RandomMultihash(t), + } + var buf bytes.Buffer + require.NoError(t, in.MarshalCBOR(&buf)) + var out blob.ReleaseArguments + require.NoError(t, out.UnmarshalCBOR(&buf)) + require.Equal(t, in.Space, out.Space) + require.Equal(t, in.Digest, out.Digest) +} + // Round-trips RejectArguments through cbor. func TestRejectArgumentsRoundTrip(t *testing.T) { in := blob.RejectArguments{ diff --git a/commands/blob/cbor_gen.go b/commands/blob/cbor_gen.go index 2c1af9e..9a8f30d 100644 --- a/commands/blob/cbor_gen.go +++ b/commands/blob/cbor_gen.go @@ -1148,6 +1148,119 @@ func (t *RemoveArguments) MarshalCBOR(w io.Writer) error { cw := cbg.NewCborWriter(w) + if _, err := cw.Write([]byte{161}); err != nil { + return err + } + + // t.Digest (multihash.Multihash) (slice) + if len("digest") > 8192 { + return xerrors.Errorf("Value in field \"digest\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("digest"))); err != nil { + return err + } + if _, err := cw.WriteString(string("digest")); err != nil { + return err + } + + if len(t.Digest) > 2097152 { + return xerrors.Errorf("Byte array in field t.Digest was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Digest))); err != nil { + return err + } + + if _, err := cw.Write(t.Digest); err != nil { + return err + } + + return nil +} + +func (t *RemoveArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = RemoveArguments{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("RemoveArguments: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 6) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.Digest (multihash.Multihash) (slice) + case "digest": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 2097152 { + return fmt.Errorf("t.Digest: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Digest = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Digest); err != nil { + return err + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} +func (t *ReleaseArguments) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + if _, err := cw.Write([]byte{162}); err != nil { return err } @@ -1195,8 +1308,8 @@ func (t *RemoveArguments) MarshalCBOR(w io.Writer) error { return nil } -func (t *RemoveArguments) UnmarshalCBOR(r io.Reader) (err error) { - *t = RemoveArguments{} +func (t *ReleaseArguments) UnmarshalCBOR(r io.Reader) (err error) { + *t = ReleaseArguments{} cr := cbg.NewCborReader(r) @@ -1215,7 +1328,7 @@ func (t *RemoveArguments) UnmarshalCBOR(r io.Reader) (err error) { } if extra > cbg.MaxLength { - return fmt.Errorf("RemoveArguments: map struct too large (%d)", extra) + return fmt.Errorf("ReleaseArguments: map struct too large (%d)", extra) } n := extra @@ -1287,7 +1400,7 @@ func (t *AbortArguments) MarshalCBOR(w io.Writer) error { cw := cbg.NewCborWriter(w) - if _, err := cw.Write([]byte{163}); err != nil { + if _, err := cw.Write([]byte{162}); err != nil { return err } @@ -1307,22 +1420,6 @@ func (t *AbortArguments) MarshalCBOR(w io.Writer) error { return xerrors.Errorf("failed to write cid field t.Cause: %w", err) } - // t.Space (did.DID) (struct) - if len("space") > 8192 { - return xerrors.Errorf("Value in field \"space\" was too long") - } - - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("space"))); err != nil { - return err - } - if _, err := cw.WriteString(string("space")); err != nil { - return err - } - - if err := t.Space.MarshalCBOR(cw); err != nil { - return err - } - // t.Digest (multihash.Multihash) (slice) if len("digest") > 8192 { return xerrors.Errorf("Value in field \"digest\" was too long") @@ -1403,16 +1500,6 @@ func (t *AbortArguments) UnmarshalCBOR(r io.Reader) (err error) { t.Cause = c - } - // t.Space (did.DID) (struct) - case "space": - - { - - if err := t.Space.UnmarshalCBOR(cr); err != nil { - return xerrors.Errorf("unmarshaling t.Space: %w", err) - } - } // t.Digest (multihash.Multihash) (slice) case "digest": diff --git a/commands/blob/gen/main.go b/commands/blob/gen/main.go index 188877e..76b50a3 100644 --- a/commands/blob/gen/main.go +++ b/commands/blob/gen/main.go @@ -33,6 +33,7 @@ func main() { blob.AddArguments{}, blob.AddOK{}, blob.RemoveArguments{}, + blob.ReleaseArguments{}, blob.AbortArguments{}, blob.RejectArguments{}, blob.ReplicateArguments{}, diff --git a/commands/blob/json_gen.go b/commands/blob/json_gen.go index f94409f..38ccff2 100644 --- a/commands/blob/json_gen.go +++ b/commands/blob/json_gen.go @@ -1161,6 +1161,111 @@ func (t *RemoveArguments) MarshalDagJSON(w io.Writer) error { if err := jw.WriteObjectOpen(); err != nil { return err } + + // t.Digest (multihash.Multihash) (slice) + if len("digest") > 8192 { + return fmt.Errorf("string in field \"digest\" was too long") + } + if err := jw.WriteString(string("digest")); err != nil { + return fmt.Errorf("writing string for field \"digest\": %w", err) + } + if err := jw.WriteObjectColon(); err != nil { + return err + } + if len(t.Digest) > 2097152 { + return fmt.Errorf("byte array in field t.Digest was too long") + } + + if err := jw.WriteBytes(t.Digest); err != nil { + return fmt.Errorf("writing bytes for field t.Digest: %w", err) + } + + if err := jw.WriteObjectClose(); err != nil { + return err + } + return nil +} +func (t *RemoveArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = RemoveArguments{} + + jr := jsg.NewDagJsonReader(r) + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + if err := jr.ReadObjectOpen(); err != nil { + return fmt.Errorf("reading object open for RemoveArguments: %w", err) + } + close, err := jr.PeekObjectClose() + if err != nil { + return fmt.Errorf("peeking object close for RemoveArguments: %w", err) + } + if close { + if err := jr.ReadObjectClose(); err != nil { + return fmt.Errorf("reading object close for RemoveArguments: %w", err) + } + } else { + for i := uint64(0); i < 8192; i++ { + name, err := jr.ReadString(8192) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading string for field RemoveArguments: string too large") + } + return fmt.Errorf("reading string for field RemoveArguments: %w", err) + } + if err := jr.ReadObjectColon(); err != nil { + return fmt.Errorf("reading object colon for field RemoveArguments: %w", err) + } + switch name { + + // t.Digest (multihash.Multihash) (slice) + case "digest": + + { + bval, err := jr.ReadBytes(2097152) + if err != nil { + if errors.Is(err, jsg.ErrLimitExceeded) { + return fmt.Errorf("reading bytes for field t.Digest: byte array too large") + } + return fmt.Errorf("reading bytes for field t.Digest: %w", err) + } + if len(bval) > 0 { + t.Digest = []uint8(bval) + } + } + + default: + // Field doesn't exist on this type, so ignore it + if err := jr.DiscardType(); err != nil { + return fmt.Errorf("ignoring field %s for RemoveArguments: %w", name, err) + } + } + + close, err := jr.ReadObjectCloseOrComma() + if err != nil { + return fmt.Errorf("reading object close or comma for field RemoveArguments: %w", err) + } + if close { + break + } + if i == 8192-1 { + return fmt.Errorf("map too large for RemoveArguments") + } + } + } + + return nil +} +func (t *ReleaseArguments) MarshalDagJSON(w io.Writer) error { + jw := jsg.NewDagJsonWriter(w) + if t == nil { + err := jw.WriteNull() + return err + } + if err := jw.WriteObjectOpen(); err != nil { + return err + } written := 0 // t.Digest (multihash.Multihash) (slice) @@ -1207,8 +1312,8 @@ func (t *RemoveArguments) MarshalDagJSON(w io.Writer) error { } return nil } -func (t *RemoveArguments) UnmarshalDagJSON(r io.Reader) (err error) { - *t = RemoveArguments{} +func (t *ReleaseArguments) UnmarshalDagJSON(r io.Reader) (err error) { + *t = ReleaseArguments{} jr := jsg.NewDagJsonReader(r) defer func() { @@ -1217,27 +1322,27 @@ func (t *RemoveArguments) UnmarshalDagJSON(r io.Reader) (err error) { } }() if err := jr.ReadObjectOpen(); err != nil { - return fmt.Errorf("reading object open for RemoveArguments: %w", err) + return fmt.Errorf("reading object open for ReleaseArguments: %w", err) } close, err := jr.PeekObjectClose() if err != nil { - return fmt.Errorf("peeking object close for RemoveArguments: %w", err) + return fmt.Errorf("peeking object close for ReleaseArguments: %w", err) } if close { if err := jr.ReadObjectClose(); err != nil { - return fmt.Errorf("reading object close for RemoveArguments: %w", err) + return fmt.Errorf("reading object close for ReleaseArguments: %w", err) } } else { for i := uint64(0); i < 8192; i++ { name, err := jr.ReadString(8192) if err != nil { if errors.Is(err, jsg.ErrLimitExceeded) { - return fmt.Errorf("reading string for field RemoveArguments: string too large") + return fmt.Errorf("reading string for field ReleaseArguments: string too large") } - return fmt.Errorf("reading string for field RemoveArguments: %w", err) + return fmt.Errorf("reading string for field ReleaseArguments: %w", err) } if err := jr.ReadObjectColon(); err != nil { - return fmt.Errorf("reading object colon for field RemoveArguments: %w", err) + return fmt.Errorf("reading object colon for field ReleaseArguments: %w", err) } switch name { @@ -1267,19 +1372,19 @@ func (t *RemoveArguments) UnmarshalDagJSON(r io.Reader) (err error) { default: // Field doesn't exist on this type, so ignore it if err := jr.DiscardType(); err != nil { - return fmt.Errorf("ignoring field %s for RemoveArguments: %w", name, err) + return fmt.Errorf("ignoring field %s for ReleaseArguments: %w", name, err) } } close, err := jr.ReadObjectCloseOrComma() if err != nil { - return fmt.Errorf("reading object close or comma for field RemoveArguments: %w", err) + return fmt.Errorf("reading object close or comma for field ReleaseArguments: %w", err) } if close { break } if i == 8192-1 { - return fmt.Errorf("map too large for RemoveArguments") + return fmt.Errorf("map too large for ReleaseArguments") } } } @@ -1337,26 +1442,6 @@ func (t *AbortArguments) MarshalDagJSON(w io.Writer) error { return fmt.Errorf("writing bytes for field t.Digest: %w", err) } - written++ - if written > 0 { - if err := jw.WriteComma(); err != nil { - return err - } - } - - // t.Space (did.DID) (struct) - if len("space") > 8192 { - return fmt.Errorf("string in field \"space\" was too long") - } - if err := jw.WriteString(string("space")); err != nil { - return fmt.Errorf("writing string for field \"space\": %w", err) - } - if err := jw.WriteObjectColon(); err != nil { - return err - } - if err := t.Space.MarshalDagJSON(jw); err != nil { - return fmt.Errorf("marshaling field t.Space: %w", err) - } written++ if err := jw.WriteObjectClose(); err != nil { return err @@ -1425,13 +1510,6 @@ func (t *AbortArguments) UnmarshalDagJSON(r io.Reader) (err error) { } } - // t.Space (did.DID) (struct) - case "space": - - if err := t.Space.UnmarshalDagJSON(jr); err != nil { - return fmt.Errorf("unmarshaling t.Space: %w", err) - } - default: // Field doesn't exist on this type, so ignore it if err := jr.DiscardType(); err != nil { diff --git a/commands/blob/reject.go b/commands/blob/reject.go index a083548..9abf29e 100644 --- a/commands/blob/reject.go +++ b/commands/blob/reject.go @@ -5,23 +5,38 @@ package blob import ( "github.com/fil-forge/libforge/commands" "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/errors" "github.com/fil-forge/ucantone/ucan/command" ) type RejectOK = commands.Unit // Reject (/blob/reject) is the "don't accept" inverse of `/blob/accept`: it -// retires a PARKED blob — allocated and uploaded (HTTP PUT), never accepted. -// A blob's lifecycle on a storage node ends in exactly one of -// `/blob/accept` (commit: aggregation, location claim, registration) or -// `/blob/reject` (drop: allocation released, bytes deleted). +// retires a PARKED blob — allocated, never accepted, whether or not its +// bytes were ever received. A blob's lifecycle on a storage node ends in +// exactly one of `/blob/accept` (commit: aggregation, location claim, +// registration) or `/blob/reject` (drop: allocation released, bytes deleted). // // Served by storage nodes (subject = the provider DID, invoked by the // upload service under its registration delegation, typically translating a // client `/blob/abort`). The node drops the space's allocation and deletes -// the bytes once no space holds an allocation. A blob with any acceptance -// is refused — accepted blobs are released via `/blob/remove`. +// any received bytes once no space holds an allocation or acceptance for +// the digest. +// +// A blob that THE INVOKING SPACE has accepted is refused with BlobAccepted — +// a space's accepted blobs are released via `/blob/remove`, never rejected. +// The guard is scoped to the invoking space, not the digest: another space's +// acceptance of the same bytes must not block the reject — the node simply +// drops this space's allocation and retains the bytes for the space that +// still claims them. // // Idempotent: rejecting an unknown or already-rejected blob succeeds. // The receipt carries no payload (Unit). var Reject = binding.Bind[*RejectArguments, *RejectOK](command.MustParse("/blob/reject")) + +// BlobAcceptedErrorName is the stable receipt-failure name when reject is +// invoked for a blob the invoking space has accepted — accepted blobs are +// released via `/blob/remove`, never rejected. +const BlobAcceptedErrorName = "BlobAccepted" + +var ErrBlobAccepted = errors.New(BlobAcceptedErrorName, "blob has been accepted by the space; release the claim via /blob/remove") diff --git a/commands/blob/release.go b/commands/blob/release.go new file mode 100644 index 0000000..354df91 --- /dev/null +++ b/commands/blob/release.go @@ -0,0 +1,28 @@ +//go:build !codegen + +package blob + +import ( + "github.com/fil-forge/libforge/commands" + "github.com/fil-forge/ucantone/binding" + "github.com/fil-forge/ucantone/ucan/command" +) + +type ReleaseOK = commands.Unit + +// Release (/blob/release) is the claim-release inverse of `/blob/allocate`: +// it drops one space's reference to a blob on a storage node. It is the +// upload service's translation of a client `/blob/remove`. +// +// Served by storage nodes (subject = the provider DID, invoked by the upload +// service under its registration delegation; the space travels in the +// arguments, matching Allocate/Accept). The node drops the space's +// allocation, acceptance and location claim. Bytes are physically deleted +// only when no space claims the digest anymore — and an accepted blob's +// bytes may additionally be retained until its PDP aggregate root is fully +// retired on-chain. Physical deletion is always asynchronous: the removal +// machinery re-verifies zero claims before every destructive step. +// +// Idempotent: releasing an unknown or already-released blob succeeds. The +// receipt carries no payload (Unit). +var Release = binding.Bind[*ReleaseArguments, *ReleaseOK](command.MustParse("/blob/release")) diff --git a/commands/blob/remove.go b/commands/blob/remove.go index d0766f7..8e973ca 100644 --- a/commands/blob/remove.go +++ b/commands/blob/remove.go @@ -10,16 +10,18 @@ import ( type RemoveOK = commands.Unit -// Remove (/blob/remove) releases a space's claim on a blob. +// Remove (/blob/remove) releases a space's claim on an ACCEPTED blob. // -// Served by BOTH the upload service and storage nodes, at different levels: -// the upload service (subject = the space) validates the caller's space -// authority, deregisters the blob, and forwards the removal to every storage -// node holding it; a storage node (subject = the provider DID, invoked by the -// upload service under its registration delegation) drops the space's -// allocation/acceptance/claim and physically deletes the bytes only when no -// space claims the digest anymore (an accepted blob's bytes may additionally -// be retained until its PDP aggregate root is fully retired on-chain). +// Served by the upload service (subject = the space). The service validates +// the caller's space authority, recovers every storage node holding the blob +// (the primary via the registration's receipt chain, plus any non-failed +// replicas), forwards a `/blob/release` to each, and deregisters the blob +// last — so the receipt chain to the primary survives for a retry if every +// forward fails. Forwarding is best-effort: the node-side handler is +// idempotent and unclaimed allocations expire, so a missed node is +// reconciled by provider-side hygiene. +// +// Parked (never-accepted) blobs are abandoned via `/blob/abort` instead. // // Idempotent: removing an unknown or already-removed blob succeeds. The // receipt carries no payload (Unit). diff --git a/commands/blob/types.go b/commands/blob/types.go index c54adad..2fe30b4 100644 --- a/commands/blob/types.go +++ b/commands/blob/types.go @@ -69,30 +69,40 @@ type ListBlobItem struct { InsertedAt int64 `cborgen:"insertedAt" dagjsongen:"insertedAt"` } -// RemoveArguments releases Space's claim on the blob identified by Digest. -// Space is explicit (matching Allocate/Accept) because storage nodes key -// allocations and acceptances by (digest, space): removal drops one space's -// claim, and the node performs physical deletion only once no space claims -// the digest at all. +// RemoveArguments releases the invoking space's claim on the blob +// identified by Digest. The space is the invocation subject — it is not +// repeated in the arguments (compare ReleaseArguments, the provider-rooted +// leg, where the subject is the provider and the space must travel +// explicitly). type RemoveArguments struct { + Digest multihash.Multihash `cborgen:"digest" dagjsongen:"digest"` +} + +// ReleaseArguments drops Space's claim on the blob identified by Digest on a +// storage node. Space is explicit (matching Allocate/Accept) because the +// invocation subject is the provider, and storage nodes key allocations and +// acceptances by (digest, space): release drops one space's claim, and the +// node performs physical deletion only once no space claims the digest at +// all. +type ReleaseArguments struct { Space did.DID `cborgen:"space" dagjsongen:"space"` Digest multihash.Multihash `cborgen:"digest" dagjsongen:"digest"` } -// AbortArguments abandons Space's in-flight upload of the parked -// (never-accepted) blob identified by Digest. Cause is the -// `/space/blob/add` task link: the upload service uses it to recover which -// storage node holds the parked blob — a parked blob has no registration or -// acceptance to look the node up by. +// AbortArguments abandons the invoking space's in-flight upload of the +// parked (never-accepted) blob identified by Digest. The space is the +// invocation subject. Cause is the `/space/blob/add` task link: the upload +// service uses it to recover which storage node holds the parked blob — a +// parked blob has no registration or acceptance to look the node up by. type AbortArguments struct { - Space did.DID `cborgen:"space" dagjsongen:"space"` Digest multihash.Multihash `cborgen:"digest" dagjsongen:"digest"` Cause cid.Cid `cborgen:"cause" dagjsongen:"cause"` } // RejectArguments drops Space's allocation for the parked (never-accepted) -// blob identified by Digest on the storage node; the node deletes the bytes -// once no space holds an allocation. +// blob identified by Digest on the storage node; the node deletes any +// received bytes once no space holds an allocation or acceptance for the +// digest. type RejectArguments struct { Space did.DID `cborgen:"space" dagjsongen:"space"` Digest multihash.Multihash `cborgen:"digest" dagjsongen:"digest"` From 3e5e6ba9571103febdb1aeb92799c8f0c0ebc9f9 Mon Sep 17 00:00:00 2001 From: frrist Date: Thu, 23 Jul 2026 14:25:48 -0700 Subject: [PATCH 2/2] docs: drop the stale /space prefix from blob/add references The /space command prefix was dropped in the UCAN 1.0 transition (RFC review); the task an abort's cause links to is /blob/add. Co-Authored-By: Claude Fable 5 --- commands/blob/abort.go | 4 ++-- commands/blob/types.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/commands/blob/abort.go b/commands/blob/abort.go index 21560f3..c6b4ca8 100644 --- a/commands/blob/abort.go +++ b/commands/blob/abort.go @@ -32,9 +32,9 @@ type AbortOK = commands.Unit var Abort = binding.Bind[*AbortArguments, *AbortOK](command.MustParse("/blob/abort")) // MissingCauseErrorName is the stable receipt-failure name when an abort's -// Cause is missing or does not resolve to a known `/space/blob/add` task — +// Cause is missing or does not resolve to a known `/blob/add` task — // without it the upload service cannot recover which storage node holds the // parked blob. const MissingCauseErrorName = "MissingCause" -var ErrMissingCause = errors.New(MissingCauseErrorName, "abort requires the cause of the /space/blob/add task that parked the blob") +var ErrMissingCause = errors.New(MissingCauseErrorName, "abort requires the cause of the /blob/add task that parked the blob") diff --git a/commands/blob/types.go b/commands/blob/types.go index 2fe30b4..13cbc7b 100644 --- a/commands/blob/types.go +++ b/commands/blob/types.go @@ -91,7 +91,7 @@ type ReleaseArguments struct { // AbortArguments abandons the invoking space's in-flight upload of the // parked (never-accepted) blob identified by Digest. The space is the -// invocation subject. Cause is the `/space/blob/add` task link: the upload +// invocation subject. Cause is the `/blob/add` task link: the upload // service uses it to recover which storage node holds the parked blob — a // parked blob has no registration or acceptance to look the node up by. type AbortArguments struct {