@@ -2,13 +2,17 @@ package receipt
22
33import (
44 // for go:embed
5+
6+ "bytes"
57 _ "embed"
68 "fmt"
9+ "io"
710 "iter"
811
912 "github.com/ipld/go-ipld-prime/datamodel"
1013 "github.com/ipld/go-ipld-prime/node/bindnode"
1114 "github.com/ipld/go-ipld-prime/schema"
15+ "github.com/storacha/go-ucanto/core/car"
1216 "github.com/storacha/go-ucanto/core/dag/blockstore"
1317 "github.com/storacha/go-ucanto/core/delegation"
1418 "github.com/storacha/go-ucanto/core/invocation"
@@ -38,6 +42,10 @@ type Receipt[O, X any] interface {
3842 Issuer () ucan.Principal
3943 Proofs () delegation.Proofs
4044 Signature () signature.SignatureView
45+ Archive () io.Reader
46+ Export () iter.Seq2 [block.Block , error ]
47+ Clone () (Receipt [O , X ], error )
48+ AttachInvocation (invocation invocation.Invocation ) error
4149}
4250
4351func toResultModel [O , X any ](res result.Result [O , X ]) rdm.ResultModel [O , X ] {
@@ -55,30 +63,45 @@ func fromResultModel[O, X any](resultModel rdm.ResultModel[O, X]) result.Result[
5563 return result.Error [O , X ](* resultModel .Error )
5664}
5765
66+ var _ Receipt [any , any ] = (* receipt [any , any ])(nil )
67+
5868type receipt [O , X any ] struct {
5969 rt block.Block
6070 blks blockstore.BlockReader
6171 data * rdm.ReceiptModel [O , X ]
6272}
6373
64- var _ Receipt [any , any ] = (* receipt [any , any ])(nil )
65-
66- func (r * receipt [O , X ]) Blocks () iter.Seq2 [block.Block , error ] {
67- var iterators []iter.Seq2 [block.Block , error ]
74+ func NewReceipt [O , X any ](root ipld.Link , blocks blockstore.BlockReader , typ schema.Type , opts ... bindnode.Option ) (Receipt [O , X ], error ) {
75+ rblock , ok , err := blocks .Get (root )
76+ if err != nil {
77+ return nil , fmt .Errorf ("getting receipt root block: %w" , err )
78+ }
79+ if ! ok {
80+ return nil , fmt .Errorf ("missing receipt root block: %s" , root )
81+ }
6882
69- if inv , ok := r .Ran ().Invocation (); ok {
70- iterators = append (iterators , inv .Blocks ())
83+ rmdl := rdm.ReceiptModel [O , X ]{}
84+ err = block .Decode (rblock , & rmdl , typ , cbor .Codec , sha256 .Hasher , opts ... )
85+ if err != nil {
86+ return nil , fmt .Errorf ("decoding receipt: %w" , err )
7187 }
7288
73- for _ , prf := range r . Proofs () {
74- if delegation , ok := prf . Delegation (); ok {
75- iterators = append ( iterators , delegation . Blocks ())
76- }
89+ rcpt := receipt [ O , X ] {
90+ rt : rblock ,
91+ blks : blocks ,
92+ data : & rmdl ,
7793 }
7894
79- iterators = append (iterators , func (yield func (block.Block , error ) bool ) { yield (r .Root (), nil ) })
95+ return & rcpt , nil
96+ }
8097
81- return iterable .Concat2 (iterators ... )
98+ func NewAnyReceipt (root ipld.Link , blocks blockstore.BlockReader , opts ... bindnode.Option ) (AnyReceipt , error ) {
99+ anyReceiptType := rdm .TypeSystem ().TypeByName ("Receipt" )
100+ return NewReceipt [ipld.Node , ipld.Node ](root , blocks , anyReceiptType , opts ... )
101+ }
102+
103+ func (r * receipt [O , X ]) Blocks () iter.Seq2 [block.Block , error ] {
104+ return r .blks .Iterator ()
82105}
83106
84107func (r * receipt [O , X ]) Fx () fx.Effects {
@@ -156,33 +179,96 @@ func (r *receipt[O, X]) Signature() signature.SignatureView {
156179 return signature .NewSignatureView (signature .Decode (r .data .Sig ))
157180}
158181
159- func NewReceipt [O , X any ](root ipld.Link , blocks blockstore.BlockReader , typ schema.Type , opts ... bindnode.Option ) (Receipt [O , X ], error ) {
160- rblock , ok , err := blocks .Get (root )
182+ func (r * receipt [O , X ]) Archive () io.Reader {
183+ // We create a descriptor block to describe what this DAG represents
184+ variant , err := block .Encode (
185+ & rdm.ArchiveModel {UcanReceipt0_9_1 : r .rt .Link ()},
186+ rdm .ArchiveType (),
187+ cbor .Codec ,
188+ sha256 .Hasher ,
189+ )
161190 if err != nil {
162- return nil , fmt .Errorf ("getting receipt root block: %w" , err )
191+ reader , _ := io .Pipe ()
192+ reader .CloseWithError (fmt .Errorf ("hashing variant block bytes: %w" , err ))
193+ return reader
163194 }
164- if ! ok {
165- return nil , fmt .Errorf ("missing receipt root block: %s" , root )
195+
196+ return car .Encode ([]ipld.Link {variant .Link ()}, func (yield func (ipld.Block , error ) bool ) {
197+ for b , err := range r .Export () {
198+ if ! yield (b , err ) || err != nil {
199+ return
200+ }
201+ }
202+ yield (variant , nil )
203+ })
204+ }
205+
206+ // Export ONLY the blocks that comprise the receipt, its original invocation and its proofs
207+ // This differs from Blocks(), which simply returns all the blocks in the backing blockstore
208+ func (r * receipt [O , X ]) Export () iter.Seq2 [block.Block , error ] {
209+ var iterators []iter.Seq2 [block.Block , error ]
210+
211+ if inv , ok := r .Ran ().Invocation (); ok {
212+ iterators = append (iterators , inv .Export ())
166213 }
167214
168- rmdl := rdm.ReceiptModel [O , X ]{}
169- err = block .Decode (rblock , & rmdl , typ , cbor .Codec , sha256 .Hasher , opts ... )
215+ for _ , prf := range r .Proofs () {
216+ if delegation , ok := prf .Delegation (); ok {
217+ iterators = append (iterators , delegation .Export ())
218+ }
219+ }
220+
221+ iterators = append (iterators , func (yield func (block.Block , error ) bool ) { yield (r .Root (), nil ) })
222+
223+ return iterable .Concat2 (iterators ... )
224+ }
225+
226+ // Clone returns a new Receipt by copying r's backing blockstore.
227+ func (r * receipt [O , X ]) Clone () (Receipt [O , X ], error ) {
228+ blks , err := blockstore .NewBlockStore (blockstore .WithBlocksIterator (r .blks .Iterator ()))
170229 if err != nil {
171- return nil , fmt .Errorf ("decoding receipt : %w" , err )
230+ return nil , fmt .Errorf ("creating block reader : %w" , err )
172231 }
232+ return & receipt [O , X ]{
233+ rt : r .rt ,
234+ blks : blks ,
235+ data : r .data ,
236+ }, nil
237+ }
173238
174- rcpt := receipt [O , X ]{
175- rt : rblock ,
176- blks : blocks ,
177- data : & rmdl ,
239+ // AttachInvocation adds the invocation's blocks to the receipt's blockstore.
240+ // If r already has an invocation, it returns r unchanged.
241+ // If the invocation doesn't match r's ran, it returns an error.
242+ func (r * receipt [O , X ]) AttachInvocation (invocation invocation.Invocation ) error {
243+ // confirm the invocation matches the receipt
244+ ran := r .Ran ()
245+ if ran .Link ().String () != invocation .Link ().String () {
246+ return fmt .Errorf ("expected invocation with CID %s, got %s" , ran .Link (), invocation .Link ())
178247 }
179248
180- return & rcpt , nil
181- }
249+ // don't add the invocation if it's already there
250+ if _ , ok := ran .Invocation (); ok {
251+ return nil
252+ }
182253
183- func NewAnyReceipt (root ipld.Link , blocks blockstore.BlockReader , opts ... bindnode.Option ) (AnyReceipt , error ) {
184- anyReceiptType := rdm .TypeSystem ().TypeByName ("Receipt" )
185- return NewReceipt [ipld.Node , ipld.Node ](root , blocks , anyReceiptType , opts ... )
254+ // no need to copy receipt blocks if the backing BlockReader is actually a BlockStore
255+ if bs , ok := r .blks .(blockstore.BlockStore ); ok {
256+ for b , err := range invocation .Export () {
257+ if err != nil {
258+ return fmt .Errorf ("attaching invocation blocks: %w" , err )
259+ }
260+ bs .Put (b )
261+ }
262+ } else {
263+ blks , err := blockstore .NewBlockReader (blockstore .WithBlocksIterator (iterable .Concat2 (r .blks .Iterator (), invocation .Export ())))
264+ if err != nil {
265+ return fmt .Errorf ("creating block reader: %w" , err )
266+ }
267+
268+ r .blks = blks
269+ }
270+
271+ return nil
186272}
187273
188274type ReceiptReader [O , X any ] interface {
@@ -233,6 +319,46 @@ func Rebind[O, X any](from AnyReceipt, successType schema.Type, errorType schema
233319 return rdr .Read (from .Root ().Link (), from .Blocks ())
234320}
235321
322+ func Extract (b []byte ) (AnyReceipt , error ) {
323+ roots , blks , err := car .Decode (bytes .NewReader (b ))
324+ if err != nil {
325+ return nil , fmt .Errorf ("decoding CAR: %s" , err )
326+ }
327+ if len (roots ) == 0 {
328+ return nil , fmt .Errorf ("missing root CID in receipt archive" )
329+ }
330+ if len (roots ) > 1 {
331+ return nil , fmt .Errorf ("unexpected number of root CIDs in archive: %d" , len (roots ))
332+ }
333+
334+ br , err := blockstore .NewBlockReader (blockstore .WithBlocksIterator (blks ))
335+ if err != nil {
336+ return nil , fmt .Errorf ("creating block reader: %w" , err )
337+ }
338+
339+ rt , ok , err := br .Get (roots [0 ])
340+ if err != nil {
341+ return nil , fmt .Errorf ("getting root block: %w" , err )
342+ }
343+ if ! ok {
344+ return nil , fmt .Errorf ("missing root block: %s" , roots [0 ])
345+ }
346+
347+ model := rdm.ArchiveModel {}
348+ err = block .Decode (
349+ rt ,
350+ & model ,
351+ rdm .ArchiveType (),
352+ cbor .Codec ,
353+ sha256 .Hasher ,
354+ )
355+ if err != nil {
356+ return nil , fmt .Errorf ("decoding root block: %w" , err )
357+ }
358+
359+ return NewAnyReceipt (model .UcanReceipt0_9_1 , br )
360+ }
361+
236362// Option is an option configuring a UCAN delegation.
237363type Option func (cfg * receiptConfig ) error
238364
0 commit comments