Skip to content

Commit 27d8bd8

Browse files
authored
fix: use space from allocate and accept args (#19)
This is part of reverting the change to send allocate and accept delegations from the client. We decided this should in fact be made with the storage provider as the subject and that the upload service should be pre-authorized by the storage provider to invoke. This PR simply switches to using the space DID from the allocate/accept invocation args instead of the subject. It also changes the check for "upload service is invoker" to "subject is self" to ensure invocation auth is rooted in Piri and adds it (was missing) to the allocate handler. Depends on: * fil-forge/sprue#21 * fil-forge/delegator#3
1 parent f401fcb commit 27d8bd8

7 files changed

Lines changed: 114 additions & 83 deletions

File tree

cmd/cli/setup/register.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ import (
1515

1616
"github.com/BurntSushi/toml"
1717
"github.com/ethereum/go-ethereum/common"
18+
"github.com/fil-forge/libforge/commands/blob"
19+
replicacmds "github.com/fil-forge/libforge/commands/blob/replica"
20+
"github.com/fil-forge/libforge/commands/pdp"
1821
"github.com/fil-forge/ucantone/did"
1922
"github.com/fil-forge/ucantone/principal"
23+
"github.com/fil-forge/ucantone/ucan"
2024
"github.com/fil-forge/ucantone/ucan/container"
25+
"github.com/fil-forge/ucantone/ucan/delegation"
2126
logging "github.com/ipfs/go-log/v2"
2227
"github.com/samber/lo"
2328
"github.com/spf13/cobra"
@@ -796,12 +801,41 @@ func registerWithDelegator(ctx context.Context, cmd *cobra.Command, cfg *appcfg.
796801
}
797802

798803
if !registered {
804+
// The delegator's register-node handler requires a UCAN container of
805+
// proofs: self-delegations granting the upload service authority over
806+
// the blob/pdp capabilities on this provider. They never expire.
807+
uploadDID := flags.baseConfig.uploadServiceDID
808+
if uploadDID == did.Undef {
809+
return "", "", fmt.Errorf("upload service DID is not configured")
810+
}
811+
self := cfg.Identity.Signer
812+
813+
cmds := []ucan.Command{
814+
blob.Allocate.Command,
815+
blob.Accept.Command,
816+
pdp.Info.Command,
817+
replicacmds.Allocate.Command,
818+
}
819+
dlgs := make([]ucan.Delegation, 0, len(cmds))
820+
for _, cmd := range cmds {
821+
dlg, err := delegation.Delegate(self, uploadDID, self.DID(), cmd, delegation.WithNoExpiration())
822+
if err != nil {
823+
return "", "", fmt.Errorf("creating %s delegation: %w", cmd, err)
824+
}
825+
dlgs = append(dlgs, dlg)
826+
}
827+
proofsBytes, err := container.Encode(container.Base64Gzip, container.New(container.WithDelegations(dlgs...)))
828+
if err != nil {
829+
return "", "", fmt.Errorf("encoding provider proofs: %w", err)
830+
}
831+
799832
if err := c.Register(ctx, &delgclient.RegisterRequest{
800833
Operator: operatorDID,
801834
OwnerAddress: ownerAddress.String(),
802835
ProofSetID: proofSetID,
803836
OperatorEmail: flags.operatorEmail,
804837
PublicURL: flags.publicURL.String(),
838+
Proofs: string(proofsBytes),
805839
}); err != nil {
806840
return "", "", fmt.Errorf("registering with delegator: %w", err)
807841
}

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ require (
1212
github.com/deckarep/golang-set/v2 v2.6.0
1313
github.com/docker/docker v28.5.1+incompatible
1414
github.com/ethereum/go-ethereum v1.16.7
15-
github.com/fil-forge/delegator v0.0.0-20260527145843-e103ee9f563f
15+
github.com/fil-forge/delegator v0.0.0-20260619085531-c96197bec34c
1616
github.com/fil-forge/filecoin-services/go v0.0.0-20260507172456-36ebe4467390
1717
github.com/fil-forge/go-ipni-tools v0.0.0-20260519194815-545b9421aec0
18-
github.com/fil-forge/libforge v0.0.0-20260527182359-ebb22552c348
18+
github.com/fil-forge/libforge v0.0.0-20260619084920-1753f2265c95
1919
github.com/fil-forge/piri-signing-service v0.0.0-20260527011208-918512802357
2020
github.com/fil-forge/ucantone v0.0.0-20260527115858-517b03bc3c72
2121
github.com/filecoin-project/go-address v1.2.0

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -545,16 +545,16 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
545545
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
546546
github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY=
547547
github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
548-
github.com/fil-forge/delegator v0.0.0-20260527145843-e103ee9f563f h1:ZPyzLZcgT6VIiM/5BGM+JdtB2MaQk3kS+za0pXFrfzo=
549-
github.com/fil-forge/delegator v0.0.0-20260527145843-e103ee9f563f/go.mod h1:IRZ6E9Vg3Z7UeWMgHUVb94Pn1wELAHKTPTpBigfH+WI=
548+
github.com/fil-forge/delegator v0.0.0-20260619085531-c96197bec34c h1:YhtqnvFK7HqbZ4rzZrpm9wO8a9674C8HStWknoSi5UM=
549+
github.com/fil-forge/delegator v0.0.0-20260619085531-c96197bec34c/go.mod h1:Mkd7wcTvxuWzfVwgRIx/xfnwMZM9Hrd7+p6N1dgI0no=
550550
github.com/fil-forge/filecoin-services/go v0.0.0-20260507172456-36ebe4467390 h1:h7SZVboCO1iPDtdxPJjs1fj4IVFq/dpDWFevkXtwgps=
551551
github.com/fil-forge/filecoin-services/go v0.0.0-20260507172456-36ebe4467390/go.mod h1:ur5fRrG8v9twwwRFdX7lquGza4liT6x/xAFW7T4XVIc=
552552
github.com/fil-forge/go-ipni-tools v0.0.0-20260519194815-545b9421aec0 h1:HAfXUPvtPjK9UAzofbDSzW0mSXsbubMtIFCEh0SJ2ow=
553553
github.com/fil-forge/go-ipni-tools v0.0.0-20260519194815-545b9421aec0/go.mod h1:3NRV/7wc4/0uzzrGdI7NoN/yeF1UvqKRwMyjBqGc5s0=
554554
github.com/fil-forge/go-ucanto v0.0.0-20260507172450-5cb5d073f8ab h1:2J2cDThqTKP6/0k3SfdlSxfyPa3aLqjTYnmvbEcryfg=
555555
github.com/fil-forge/go-ucanto v0.0.0-20260507172450-5cb5d073f8ab/go.mod h1:lZF3UXZ2hGLKYmXdquG50JqI9pRlUrV6lubGtgOYfwc=
556-
github.com/fil-forge/libforge v0.0.0-20260527182359-ebb22552c348 h1:roYe4llNv0fpQnvXMontrdt/hy9kH5K/cnNsXmhqId4=
557-
github.com/fil-forge/libforge v0.0.0-20260527182359-ebb22552c348/go.mod h1:1ytnrneNEeJcskEbsRDtNZY/Jvgo2Yw5szIUI/9EWPk=
556+
github.com/fil-forge/libforge v0.0.0-20260619084920-1753f2265c95 h1:GUnpBYLuWK3mzDsGVXt0CEIHIFeEd3B+Ne2FkqnBfJU=
557+
github.com/fil-forge/libforge v0.0.0-20260619084920-1753f2265c95/go.mod h1:1ytnrneNEeJcskEbsRDtNZY/Jvgo2Yw5szIUI/9EWPk=
558558
github.com/fil-forge/piri-signing-service v0.0.0-20260527011208-918512802357 h1:sTK8Yc/kds7MkG0cpK5DGDtDCtU1ZwgQWc4OzRf3ZP4=
559559
github.com/fil-forge/piri-signing-service v0.0.0-20260527011208-918512802357/go.mod h1:fgg5hE/BlnFlR5qXsT0POv6GWVu7cj526s7v2+mdJXo=
560560
github.com/fil-forge/ucantone v0.0.0-20260527115858-517b03bc3c72 h1:FFK4CC7IfLfwa5ZQExdkZayPc6Tg0JgVK4jhn2hd2cY=

pkg/ucanhandlers/blob/accept.go

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,36 +20,29 @@ import (
2020
"github.com/fil-forge/libforge/commands/blob"
2121
"github.com/fil-forge/libforge/commands/pdp"
2222
"github.com/fil-forge/ucantone/did"
23-
"github.com/fil-forge/ucantone/errors"
2423
"github.com/fil-forge/ucantone/principal"
2524
"github.com/fil-forge/ucantone/ucan"
2625
"github.com/fil-forge/ucantone/ucan/invocation"
2726
"github.com/fil-forge/ucantone/ucan/promise"
2827

29-
"github.com/fil-forge/piri/pkg/config/app"
3028
"github.com/fil-forge/piri/pkg/pdp/aggregation/commp"
3129
pdptypes "github.com/fil-forge/piri/pkg/pdp/types"
3230
"github.com/fil-forge/piri/pkg/service/publisher"
3331
"github.com/fil-forge/piri/pkg/store/acceptancestore"
3432
"github.com/fil-forge/piri/pkg/store/acceptancestore/acceptance"
3533
"github.com/fil-forge/piri/pkg/store/invocationstore"
34+
"github.com/fil-forge/piri/pkg/ucanhandlers"
3635
)
3736

3837
// InternalErrorName is the stable receipt-failure name for invariant
3938
// violations the handler hits at runtime (e.g., a link type we expected
4039
// to be a cidlink wasn't).
4140
const InternalErrorName = "InternalError"
4241

43-
// InvalidCauseErrorName is the stable receipt-failure name for a
44-
// /blob/accept invocation issued by a principal other than the upload
45-
// service.
46-
const InvalidCauseErrorName = "InvalidCause"
47-
4842
// AcceptDeps is the dependency set populated by fx for the Accept handler.
4943
type AcceptDeps struct {
5044
fx.In
5145
ID principal.Signer
52-
Upload app.UploadServiceConfig
5346
Acceptances AcceptanceStore
5447
Pieces PieceReader
5548
Commp commp.Calculator
@@ -78,22 +71,15 @@ func NewAcceptHandler(deps AcceptDeps) server.Route {
7871
return blob.Accept.Route(func(req *binding.Request[*blob.AcceptArguments], rsp *binding.Response[*blob.AcceptOK]) error {
7972
args := req.Task().Arguments()
8073

81-
// /blob/accept is performed by the upload service, so the only
82-
// authorization required here is that the invocation issuer is
83-
// the upload service.
84-
// TODO(forrest)[ucan1]: confirm how this relates to the proof
85-
// chain originating from guppy — is that chain validated
86-
// elsewhere, and does it make this issuer check redundant or
87-
// complementary?
88-
if iss := req.Invocation().Issuer(); iss != deps.Upload.DID {
89-
return rsp.SetFailure(errors.New(
90-
InvalidCauseErrorName,
91-
"issuer is %s not the upload service %s", iss, deps.Upload.DID,
92-
))
74+
// The invocation subject must be this storage provider — the proofs are
75+
// rooted at the provider, so authorization for the invoker to call
76+
// `/blob/accept` is enforced by the validator's proof chain.
77+
if err := ucanhandlers.RequireSubject(req, deps.ID.DID()); err != nil {
78+
return rsp.SetFailure(err)
9379
}
9480

9581
resp, err := Accept(req.Context(), deps, &AcceptRequest{
96-
Space: req.Task().Subject(),
82+
Space: args.Space,
9783
Blob: blob.Blob{
9884
Digest: args.Blob.Digest,
9985
Size: args.Blob.Size,

pkg/ucanhandlers/blob/allocate.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/fil-forge/piri/pkg/store"
2929
"github.com/fil-forge/piri/pkg/store/allocationstore"
3030
"github.com/fil-forge/piri/pkg/store/allocationstore/allocation"
31+
"github.com/fil-forge/piri/pkg/ucanhandlers"
3132
)
3233

3334
var log = logging.Logger("storage/handlers/blob")
@@ -74,10 +75,13 @@ func NewBlobAllocateHandler(deps AllocateDeps) server.Route {
7475
return blob.Allocate.Route(func(req *binding.Request[*blob.AllocateArguments], rsp *binding.Response[*blob.AllocateOK]) error {
7576
args := req.Task().Arguments()
7677

77-
// /blob/allocate is space-scoped: the invocation subject IS
78-
// the space being allocated into. Authorization is enforced
79-
// by the validator's proof chain, not by an issuer-equals-
80-
// service check.
78+
// The invocation subject must be this storage provider; the space being
79+
// allocated into travels in the arguments. Authorization that the upload
80+
// service may invoke /blob/allocate is enforced by the validator's proof
81+
// chain (rooted at the provider).
82+
if err := ucanhandlers.RequireSubject(req, deps.ID.Signer.DID()); err != nil {
83+
return rsp.SetFailure(err)
84+
}
8185

8286
// TODO(forrest)[ucan1]: reconcile with blob.MaxBlobSize
8387
// to ensure it matches the constraints of piri, namely the aggregation pipeline to adding roots
@@ -89,7 +93,7 @@ func NewBlobAllocateHandler(deps AllocateDeps) server.Route {
8993
}
9094

9195
resp, err := Allocate(req.Context(), deps, &AllocateRequest{
92-
Space: req.Task().Subject(),
96+
Space: args.Space,
9397
Blob: args.Blob,
9498
Cause: args.Cause,
9599
})

pkg/ucanhandlers/ucanfxtest/rpc/blob_accept_test.go

Lines changed: 46 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@ import (
1616
"github.com/stretchr/testify/require"
1717
)
1818

19-
// /blob/accept is performed by the upload service: the handler requires the
20-
// invocation issuer to be the upload service DID and reads the space from the
21-
// invocation subject (authorized by a space->upload-service delegation). The
22-
// handler:
19+
// /blob/accept is performed by the upload service: The invocation subject is
20+
// the storage provider (authorized by a provider->upload-service delegation)
21+
// and the space travels in AcceptArguments.Space. The handler:
2322
// - looks up the blob bytes in the piece store (Has check)
2423
// - writes an acceptance record carrying a /pdp/accept promise
2524
// - issues a /assert/location claim and persists it in the invocation store
@@ -42,20 +41,22 @@ func (s *RPCSuite) TestBlobAccept_Basic() {
4241
// hand it a freshly-minted CID — the handler doesn't resolve the
4342
// promise, it just persists it alongside the acceptance record.
4443
putAwait := promise.AwaitOK{Task: testutil.RandomCID(t)}
44+
space := testutil.RandomDID(t)
4545

46-
// /blob/accept is performed by the upload service: the space delegates
47-
// /blob/accept to the upload service (the guppy chain), which then
48-
// issues the invocation with the space as its subject and the
49-
// delegation as proof.
46+
// /blob/accept is performed by the upload service: the provider delegates
47+
// /blob/accept to the upload service, which issues the invocation with the
48+
// provider as its subject and the delegation as proof. The space travels in
49+
// the arguments.
5050
proof := testutil.Must(delegation.Delegate(
5151
s.ServiceID, s.UploadServiceIdentity.DID(), s.ServiceID.DID(), blob.Accept.Command,
5252
))(t)
5353
inv := testutil.Must(blob.Accept.Invoke(
5454
s.UploadServiceIdentity,
5555
s.ServiceID.DID(),
5656
&blob.AcceptArguments{
57-
Blob: blob.Blob{Digest: digest, Size: size},
58-
Put: putAwait,
57+
Space: space,
58+
Blob: blob.Blob{Digest: digest, Size: size},
59+
Put: putAwait,
5960
},
6061
invocation.WithAudience(s.ServiceID.DID()),
6162
invocation.WithProofs(proof.Link()),
@@ -66,11 +67,11 @@ func (s *RPCSuite) TestBlobAccept_Basic() {
6667
require.NotEqual(t, cid.Undef, ok.Site, "AcceptOK.Site is the location claim CID")
6768

6869
// Acceptance store carries the accepted blob + space + cause.
69-
acc, err := s.Acceptances.Get(t.Context(), digest, s.ServiceID.DID())
70+
acc, err := s.Acceptances.Get(t.Context(), digest, space)
7071
require.NoError(t, err, "acceptance persisted")
7172
require.Equal(t, digest, acc.Blob.Digest)
7273
require.Equal(t, size, acc.Blob.Size)
73-
require.Equal(t, s.ServiceID.DID(), acc.Space)
74+
require.Equal(t, space, acc.Space)
7475
require.Equal(t, inv.Task().Link(), acc.Cause, "cause records the accept task link")
7576
require.NotNil(t, acc.PDPAccept, "PDP accept promise is recorded for aggregation completion")
7677

@@ -79,7 +80,7 @@ func (s *RPCSuite) TestBlobAccept_Basic() {
7980
require.NoError(t, err, "location claim persisted under Site CID")
8081
require.Equal(t, s.ServiceID.DID(), claim.Issuer(), "claim is signed by the service")
8182
require.Equal(t, s.ServiceID.DID(), claim.Subject(),
82-
"location claim is scoped to the service in the current single-space handler")
83+
"location claim is issued by the provider node")
8384
require.Equal(t, assert.Location.Command, claim.Command(),
8485
"claim is an /assert/location invocation")
8586

@@ -89,6 +90,7 @@ func (s *RPCSuite) TestBlobAccept_Basic() {
8990
require.NoError(t, locArgs.UnmarshalCBOR(bytes.NewReader(claim.ArgumentsBytes())),
9091
"location claim args decode")
9192
require.Equal(t, digest, locArgs.Content, "claim references the accepted blob digest")
93+
require.Equal(t, space, locArgs.Space, "location claim is scoped to the space from the args")
9294
expectedURL, err := s.Pieces.ReadPieceURL(cid.NewCidV1(cid.Raw, digest))
9395
require.NoError(t, err)
9496
require.Len(t, locArgs.Location, 1)
@@ -117,18 +119,25 @@ func (s *RPCSuite) TestBlobAccept_ExistingDataInDifferentSpace() {
117119
digest := testutil.Must(multihash.Sum(data, multihash.SHA2_256, -1))(t)
118120
size := uint64(len(data))
119121

120-
// Two spaces, each its own did:key signer. Each self-issues its
121-
// /blob/allocate (issuer == subject, no proof chain) but delegates
122-
// /blob/accept to the upload service, which issues that invocation.
123-
spaceA := testutil.RandomSigner(t)
124-
spaceB := testutil.RandomSigner(t)
122+
// Two spaces, each just a DID carried in the invocation arguments. Every
123+
// invocation's subject is the provider. /blob/allocate is self-issued by the
124+
// provider (issuer == subject, no proof chain); /blob/accept is issued by the
125+
// upload service and authorized by a single provider->upload-service
126+
// delegation, reused across spaces since it is not space-scoped.
127+
spaceA := testutil.RandomDID(t)
128+
spaceB := testutil.RandomDID(t)
129+
130+
acceptProof := testutil.Must(delegation.Delegate(
131+
s.ServiceID, s.UploadServiceIdentity.DID(), service, blob.Accept.Command,
132+
))(t)
125133

126134
// --- spaceA: first allocation, then upload, then accept ---
127135

128136
allocA := testutil.Must(blob.Allocate.Invoke(
129-
spaceA,
130-
spaceA.DID(),
137+
s.ServiceID,
138+
service,
131139
&blob.AllocateArguments{
140+
Space: spaceA,
132141
Blob: blob.Blob{Digest: digest, Size: size},
133142
Cause: testutil.RandomCID(t),
134143
},
@@ -141,30 +150,26 @@ func (s *RPCSuite) TestBlobAccept_ExistingDataInDifferentSpace() {
141150
// Simulate the upload completing.
142151
s.Pieces.Put(digest, data)
143152

144-
// /blob/accept is issued by the upload service; each space delegates
145-
// the capability to it (the guppy chain) and the delegation rides
146-
// along as proof.
147-
acceptProofA := testutil.Must(delegation.Delegate(
148-
spaceA, s.UploadServiceIdentity.DID(), spaceA.DID(), blob.Accept.Command,
149-
))(t)
150153
acceptA := testutil.Must(blob.Accept.Invoke(
151154
s.UploadServiceIdentity,
152-
spaceA.DID(),
155+
service,
153156
&blob.AcceptArguments{
154-
Blob: blob.Blob{Digest: digest, Size: size},
155-
Put: promise.AwaitOK{Task: testutil.RandomCID(t)},
157+
Space: spaceA,
158+
Blob: blob.Blob{Digest: digest, Size: size},
159+
Put: promise.AwaitOK{Task: testutil.RandomCID(t)},
156160
},
157161
invocation.WithAudience(service),
158-
invocation.WithProofs(acceptProofA.Link()),
162+
invocation.WithProofs(acceptProof.Link()),
159163
))(t)
160-
acceptOKA := decodeAcceptOK(t, s.sendInvocationWithProofs(t, acceptA, acceptProofA))
164+
acceptOKA := decodeAcceptOK(t, s.sendInvocationWithProofs(t, acceptA, acceptProof))
161165

162166
// --- spaceB: bytes are already present from spaceA's upload ---
163167

164168
allocB := testutil.Must(blob.Allocate.Invoke(
165-
spaceB,
166-
spaceB.DID(),
169+
s.ServiceID,
170+
service,
167171
&blob.AllocateArguments{
172+
Space: spaceB,
168173
Blob: blob.Blob{Digest: digest, Size: size},
169174
Cause: testutil.RandomCID(t),
170175
},
@@ -176,20 +181,18 @@ func (s *RPCSuite) TestBlobAccept_ExistingDataInDifferentSpace() {
176181
require.Nil(t, okB.Address,
177182
"bytes already in store from spaceA — no upload URL for spaceB")
178183

179-
acceptProofB := testutil.Must(delegation.Delegate(
180-
spaceB, s.UploadServiceIdentity.DID(), spaceB.DID(), blob.Accept.Command,
181-
))(t)
182184
acceptB := testutil.Must(blob.Accept.Invoke(
183185
s.UploadServiceIdentity,
184-
spaceB.DID(),
186+
service,
185187
&blob.AcceptArguments{
186-
Blob: blob.Blob{Digest: digest, Size: size},
187-
Put: promise.AwaitOK{Task: testutil.RandomCID(t)},
188+
Space: spaceB,
189+
Blob: blob.Blob{Digest: digest, Size: size},
190+
Put: promise.AwaitOK{Task: testutil.RandomCID(t)},
188191
},
189192
invocation.WithAudience(service),
190-
invocation.WithProofs(acceptProofB.Link()),
193+
invocation.WithProofs(acceptProof.Link()),
191194
))(t)
192-
acceptOKB := decodeAcceptOK(t, s.sendInvocationWithProofs(t, acceptB, acceptProofB))
195+
acceptOKB := decodeAcceptOK(t, s.sendInvocationWithProofs(t, acceptB, acceptProof))
193196

194197
// --- both spaces have independent records keyed on (digest, space) ---
195198

@@ -198,8 +201,8 @@ func (s *RPCSuite) TestBlobAccept_ExistingDataInDifferentSpace() {
198201
site cid.Cid
199202
cause cid.Cid
200203
}{
201-
{spaceA.DID(), acceptOKA.Site, acceptA.Task().Link()},
202-
{spaceB.DID(), acceptOKB.Site, acceptB.Task().Link()},
204+
{spaceA, acceptOKA.Site, acceptA.Task().Link()},
205+
{spaceB, acceptOKB.Site, acceptB.Task().Link()},
203206
} {
204207
alloc, err := s.Allocations.Get(t.Context(), digest, sp.space)
205208
require.NoError(t, err, "allocation persisted under space=%s", sp.space)

0 commit comments

Comments
 (0)