@@ -18,6 +18,7 @@ import (
1818 "github.com/fil-forge/ucantone/ucan/invocation"
1919 block "github.com/ipfs/go-block-format"
2020 "github.com/ipfs/go-cid"
21+ mh "github.com/multiformats/go-multihash"
2122 "go.uber.org/zap"
2223
2324 "github.com/fil-forge/ingot/blockstore/locator"
@@ -47,13 +48,21 @@ type Forge struct {
4748 logger * zap.Logger
4849}
4950
50- var _ BlockReader = (* Forge )(nil )
51+ var (
52+ _ BlockReader = (* Forge )(nil )
53+ _ BlobReader = (* Forge )(nil )
54+ )
5155
5256// ForgeConfig wires a read-only Forge block reader.
5357type ForgeConfig struct {
54- // IndexerEndpoint is the indexing-service URL.
58+ // Locator, when set, resolves blob locations instead of the indexing-service
59+ // — the appliance read tier (e.g. registry.LocalLocator over blob_locations).
60+ // When nil, an indexing-service-backed locator is built from
61+ // IndexerEndpoint/IndexerDID. Either way the retrieval path is identical.
62+ Locator locator.Locator
63+ // IndexerEndpoint is the indexing-service URL. Required only when Locator is nil.
5564 IndexerEndpoint string
56- // IndexerDID is the indexing-service principal.
65+ // IndexerDID is the indexing-service principal. Required only when Locator is nil.
5766 IndexerDID string
5867 // Spaces scopes the locator queries; for ingot this is the single space it owns.
5968 Spaces []did.DID
@@ -69,15 +78,10 @@ type ForgeConfig struct {
6978 Logger * zap.Logger
7079}
7180
72- // NewForge constructs a Forge block reader, building an indexing-service client
73- // wrapped by the index locator.
81+ // NewForge constructs a Forge block reader. It uses cfg.Locator when set (the
82+ // appliance read tier); otherwise it builds an indexing-service-backed locator
83+ // from IndexerEndpoint/IndexerDID.
7484func NewForge (cfg ForgeConfig ) (* Forge , error ) {
75- if cfg .IndexerEndpoint == "" {
76- return nil , errors .New ("forge blockstore: indexer endpoint is required" )
77- }
78- if cfg .IndexerDID == "" {
79- return nil , errors .New ("forge blockstore: indexer DID is required" )
80- }
8185 if len (cfg .Spaces ) == 0 {
8286 return nil , errors .New ("forge blockstore: at least one space is required" )
8387 }
@@ -88,15 +92,6 @@ func NewForge(cfg ForgeConfig) (*Forge, error) {
8892 return nil , errors .New ("forge blockstore: space signer is required" )
8993 }
9094
91- endpointURL , err := url .Parse (cfg .IndexerEndpoint )
92- if err != nil {
93- return nil , fmt .Errorf ("forge blockstore: parse indexer endpoint: %w" , err )
94- }
95- indexerDID , err := did .Parse (cfg .IndexerDID )
96- if err != nil {
97- return nil , fmt .Errorf ("forge blockstore: parse indexer DID: %w" , err )
98- }
99-
10095 httpc := cfg .HTTPClient
10196 if httpc == nil {
10297 httpc = http .DefaultClient
@@ -106,14 +101,30 @@ func NewForge(cfg ForgeConfig) (*Forge, error) {
106101 logger = zap .NewNop ()
107102 }
108103
109- idxClient , err := indexclient .New (indexerDID , * endpointURL , indexclient .WithHTTPClient (httpc ))
110- if err != nil {
111- return nil , fmt .Errorf ("forge blockstore: build indexing-service client: %w" , err )
104+ loc := cfg .Locator
105+ if loc == nil {
106+ // No locator injected: resolve through the indexing-service.
107+ if cfg .IndexerEndpoint == "" {
108+ return nil , errors .New ("forge blockstore: indexer endpoint is required (no locator injected)" )
109+ }
110+ if cfg .IndexerDID == "" {
111+ return nil , errors .New ("forge blockstore: indexer DID is required (no locator injected)" )
112+ }
113+ endpointURL , err := url .Parse (cfg .IndexerEndpoint )
114+ if err != nil {
115+ return nil , fmt .Errorf ("forge blockstore: parse indexer endpoint: %w" , err )
116+ }
117+ indexerDID , err := did .Parse (cfg .IndexerDID )
118+ if err != nil {
119+ return nil , fmt .Errorf ("forge blockstore: parse indexer DID: %w" , err )
120+ }
121+ idxClient , err := indexclient .New (indexerDID , * endpointURL , indexclient .WithHTTPClient (httpc ))
122+ if err != nil {
123+ return nil , fmt .Errorf ("forge blockstore: build indexing-service client: %w" , err )
124+ }
125+ loc = locator .NewIndexLocator (idxClient , newAuthorizeRetrieval (cfg .SpaceSigner , indexerDID ))
112126 }
113127
114- authFn := newAuthorizeRetrieval (cfg .SpaceSigner , indexerDID )
115- loc := locator .NewIndexLocator (idxClient , authFn )
116-
117128 return & Forge {
118129 locator : loc ,
119130 signer : cfg .Signer ,
@@ -124,26 +135,29 @@ func NewForge(cfg ForgeConfig) (*Forge, error) {
124135 }, nil
125136}
126137
127- // GetBlock resolves the CID through the indexer and retrieves the underlying
128- // bytes from piri via a UCAN-authorized /content/retrieve invocation, scoped to
129- // the inner block's byte range within the containing CAR shard.
130- func (f * Forge ) GetBlock (ctx context.Context , c cid.Cid ) (block.Block , error ) {
138+ // retrieve resolves c through the locator and issues a UCAN-authorized
139+ // /content/retrieve to the piri node that holds it, returning the response body
140+ // stream and the expected byte length (the location Range is inclusive, so
141+ // End-Start+1). The caller owns the returned reader and must Close it. Shared by
142+ // GetBlock (which buffers small catalog blocks) and OpenBlob (which streams large
143+ // body blobs straight through).
144+ func (f * Forge ) retrieve (ctx context.Context , c cid.Cid ) (io.ReadCloser , int64 , error ) {
131145 locations , err := f .locator .Locate (ctx , f .spaces , c .Hash ())
132146 if err != nil {
133147 var nf locator.NotFoundError
134148 if errors .As (err , & nf ) {
135- return nil , ErrNotFound
149+ return nil , 0 , ErrNotFound
136150 }
137- return nil , fmt .Errorf ("forge: locate %s: %w" , c , err )
151+ return nil , 0 , fmt .Errorf ("forge: locate %s: %w" , c , err )
138152 }
139153 if len (locations ) == 0 {
140- return nil , ErrNotFound
154+ return nil , 0 , ErrNotFound
141155 }
142156
143157 loc := locations [0 ]
144158 cm := loc .Commitment
145159 if len (cm .Location ) == 0 {
146- return nil , fmt .Errorf ("forge: empty location URL set for %s" , c )
160+ return nil , 0 , fmt .Errorf ("forge: empty location URL set for %s" , c )
147161 }
148162 target := cm .Location [0 ]
149163
@@ -166,7 +180,7 @@ func (f *Forge) GetBlock(ctx context.Context, c cid.Cid) (block.Block, error) {
166180 delegation .WithExpiration (ucan .Now ()+ retrievalAuthTTL ),
167181 )
168182 if err != nil {
169- return nil , fmt .Errorf ("forge: build retrieval proof: %w" , err )
183+ return nil , 0 , fmt .Errorf ("forge: build retrieval proof: %w" , err )
170184 }
171185
172186 inv , err := contentcmds .Retrieve .Invoke (
@@ -180,41 +194,59 @@ func (f *Forge) GetBlock(ctx context.Context, c cid.Cid) (block.Block, error) {
180194 invocation .WithProofs (retrievalProof .Link ()),
181195 )
182196 if err != nil {
183- return nil , fmt .Errorf ("forge: build retrieve invocation: %w" , err )
197+ return nil , 0 , fmt .Errorf ("forge: build retrieve invocation: %w" , err )
184198 }
185199
186200 rclient , err := retrieval .NewClient (target .URL (), retrieval .WithHTTPClient (f .httpClient ))
187201 if err != nil {
188- return nil , fmt .Errorf ("forge: build retrieval client: %w" , err )
202+ return nil , 0 , fmt .Errorf ("forge: build retrieval client: %w" , err )
189203 }
190204
191205 _ , _ , meta , err := ucanexec .Execute [* contentcmds.RetrieveOK ](
192206 ctx , rclient , inv ,
193207 execution .WithDelegations (retrievalProof ),
194208 )
195209 if err != nil {
196- return nil , fmt .Errorf ("forge: retrieve %s: %w" , c , err )
210+ return nil , 0 , fmt .Errorf ("forge: retrieve %s: %w" , c , err )
197211 }
198212
199213 hcRes , ok := meta .(* retrieval.HTTPHeaderResponseContainer )
200214 if ! ok {
201- return nil , fmt .Errorf ("forge: unexpected retrieval metadata type %T" , meta )
215+ return nil , 0 , fmt .Errorf ("forge: unexpected retrieval metadata type %T" , meta )
202216 }
203- defer hcRes .Body .Close ()
217+ // Range is inclusive, so the expected length is End - Start + 1.
218+ return hcRes .Body , loc .Range .End - loc .Range .Start + 1 , nil
219+ }
204220
205- body , err := io .ReadAll (hcRes .Body )
221+ // GetBlock resolves the CID through the locator and retrieves the bytes from piri
222+ // via a UCAN-authorized /content/retrieve. It buffers the whole block, so it is
223+ // for small catalog blocks; object-body blobs use the streaming OpenBlob.
224+ func (f * Forge ) GetBlock (ctx context.Context , c cid.Cid ) (block.Block , error ) {
225+ rc , wantLen , err := f .retrieve (ctx , c )
226+ if err != nil {
227+ return nil , err
228+ }
229+ defer rc .Close ()
230+
231+ body , err := io .ReadAll (rc )
206232 if err != nil {
207233 return nil , fmt .Errorf ("forge: read retrieve body for %s: %w" , c , err )
208234 }
209- // Range is inclusive, so the expected length is End - Start + 1.
210- wantLen := loc .Range .End - loc .Range .Start + 1
211235 if int64 (len (body )) != wantLen {
212236 return nil , fmt .Errorf ("forge: %s short read: got %d bytes, want %d" , c , len (body ), wantLen )
213237 }
214-
215238 return block .NewBlockWithCid (body , c )
216239}
217240
241+ // OpenBlob streams an object-body blob from piri by digest, without buffering it
242+ // in memory — the network counterpart of Spool.OpenBlob. Bytes are served
243+ // straight off the /content/retrieve response; the caller owns the reader and
244+ // must Close it.
245+ func (f * Forge ) OpenBlob (ctx context.Context , digest mh.Multihash ) (io.ReadCloser , error ) {
246+ rc , _ , err := f .retrieve (ctx , cid .NewCidV1 (cid .Raw , digest ))
247+ return rc , err
248+ }
249+
218250// newAuthorizeRetrieval returns the AuthorizeRetrievalFunc the IndexLocator
219251// calls before each indexer query. The space signer (root authority) directly
220252// authorizes the indexer to retrieve any blob in the space — the proof chain is
0 commit comments