Skip to content

Commit dd45748

Browse files
committed
Merge branch 'main' of github.com:storacha/go-ucanto
2 parents 42a6423 + 2912062 commit dd45748

10 files changed

Lines changed: 507 additions & 36 deletions

File tree

core/receipt/datamodel/archive.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package datamodel
2+
3+
import (
4+
_ "embed"
5+
"fmt"
6+
"sync"
7+
8+
"github.com/ipld/go-ipld-prime"
9+
"github.com/ipld/go-ipld-prime/schema"
10+
)
11+
12+
//go:embed archive.ipldsch
13+
var archive []byte
14+
15+
var (
16+
once sync.Once
17+
ts *schema.TypeSystem
18+
err error
19+
)
20+
21+
func mustLoadSchema() *schema.TypeSystem {
22+
once.Do(func() {
23+
ts, err = ipld.LoadSchemaBytes(archive)
24+
})
25+
if err != nil {
26+
panic(fmt.Errorf("failed to load IPLD schema: %w", err))
27+
}
28+
return ts
29+
}
30+
31+
func ArchiveType() schema.Type {
32+
return mustLoadSchema().TypeByName("Archive")
33+
}
34+
35+
type ArchiveModel struct {
36+
UcanReceipt0_9_1 ipld.Link
37+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
type Archive struct {
2+
UcanReceipt0_9_1 Link (rename "ucan/receipt@0.9.1")
3+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package datamodel_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/ipfs/go-cid"
7+
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
8+
"github.com/storacha/go-ucanto/core/ipld/block"
9+
"github.com/storacha/go-ucanto/core/ipld/codec/cbor"
10+
"github.com/storacha/go-ucanto/core/ipld/hash/sha256"
11+
adm "github.com/storacha/go-ucanto/core/receipt/datamodel"
12+
)
13+
14+
func TestArchiveEncodeDecode(t *testing.T) {
15+
l := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")}
16+
m0 := adm.ArchiveModel{
17+
UcanReceipt0_9_1: l,
18+
}
19+
mblk, err := block.Encode(&m0, adm.ArchiveType(), cbor.Codec, sha256.Hasher)
20+
if err != nil {
21+
t.Fatalf("encoding archive model: %s", err)
22+
}
23+
24+
m1 := adm.ArchiveModel{}
25+
err = block.Decode(mblk, &m1, adm.ArchiveType(), cbor.Codec, sha256.Hasher)
26+
if err != nil {
27+
t.Fatalf("decoding agent message: %s", err)
28+
}
29+
30+
d1 := m1.UcanReceipt0_9_1
31+
if d1.String() != l.String() {
32+
t.Fatalf("failed round trip link")
33+
}
34+
}

core/receipt/datamodel/receipt_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type resultErr struct {
2020
Message string
2121
}
2222

23-
func TestEncodeDecode(t *testing.T) {
23+
func TestReceiptEncodeDecode(t *testing.T) {
2424
typ, err := rdm.NewReceiptModelType([]byte(`
2525
type Result union {
2626
| Ok "ok"

core/receipt/receipt.go

Lines changed: 155 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ package receipt
22

33
import (
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

4351
func 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+
5868
type 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

84107
func (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

188274
type 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.
237363
type Option func(cfg *receiptConfig) error
238364

0 commit comments

Comments
 (0)