github.com/storacha/go-ucanto@v0.7.2/core/receipt/receipt.go (about) 1 package receipt 2 3 import ( 4 // for go:embed 5 6 "bytes" 7 _ "embed" 8 "fmt" 9 "io" 10 "iter" 11 12 "github.com/ipld/go-ipld-prime/datamodel" 13 "github.com/ipld/go-ipld-prime/node/bindnode" 14 "github.com/ipld/go-ipld-prime/schema" 15 "github.com/storacha/go-ucanto/core/car" 16 "github.com/storacha/go-ucanto/core/dag/blockstore" 17 "github.com/storacha/go-ucanto/core/delegation" 18 "github.com/storacha/go-ucanto/core/invocation" 19 "github.com/storacha/go-ucanto/core/ipld" 20 "github.com/storacha/go-ucanto/core/ipld/block" 21 "github.com/storacha/go-ucanto/core/ipld/codec/cbor" 22 "github.com/storacha/go-ucanto/core/ipld/hash/sha256" 23 "github.com/storacha/go-ucanto/core/iterable" 24 rdm "github.com/storacha/go-ucanto/core/receipt/datamodel" 25 "github.com/storacha/go-ucanto/core/receipt/fx" 26 "github.com/storacha/go-ucanto/core/receipt/ran" 27 "github.com/storacha/go-ucanto/core/result" 28 "github.com/storacha/go-ucanto/did" 29 "github.com/storacha/go-ucanto/ucan" 30 "github.com/storacha/go-ucanto/ucan/crypto/signature" 31 ) 32 33 // Receipt represents a view of the invocation receipt. This interface provides 34 // an ergonomic API and allows you to reference linked IPLD objects if they are 35 // included in the source DAG. 36 type Receipt[O, X any] interface { 37 ipld.View 38 Ran() ran.Ran 39 Out() result.Result[O, X] 40 Fx() fx.Effects 41 Meta() map[string]any 42 Issuer() ucan.Principal 43 Proofs() delegation.Proofs 44 Signature() signature.SignatureView 45 VerifySignature(verifier signature.Verifier) (bool, error) 46 Archive() io.Reader 47 Export() iter.Seq2[block.Block, error] 48 Clone() (Receipt[O, X], error) 49 AttachInvocation(invocation invocation.Invocation) error 50 } 51 52 func toResultModel[O, X any](res result.Result[O, X]) rdm.ResultModel[O, X] { 53 return result.MatchResultR1(res, func(ok O) rdm.ResultModel[O, X] { 54 return rdm.ResultModel[O, X]{Ok: &ok, Error: nil} 55 }, func(err X) rdm.ResultModel[O, X] { 56 return rdm.ResultModel[O, X]{Ok: nil, Error: &err} 57 }) 58 } 59 60 func fromResultModel[O, X any](resultModel rdm.ResultModel[O, X]) result.Result[O, X] { 61 if resultModel.Ok != nil { 62 return result.Ok[O, X](*resultModel.Ok) 63 } 64 return result.Error[O, X](*resultModel.Error) 65 } 66 67 var _ Receipt[any, any] = (*receipt[any, any])(nil) 68 69 type receipt[O, X any] struct { 70 rt block.Block 71 blks blockstore.BlockReader 72 data *rdm.ReceiptModel[O, X] 73 } 74 75 func NewReceipt[O, X any](root ipld.Link, blocks blockstore.BlockReader, typ schema.Type, opts ...bindnode.Option) (Receipt[O, X], error) { 76 rblock, ok, err := blocks.Get(root) 77 if err != nil { 78 return nil, fmt.Errorf("getting receipt root block: %w", err) 79 } 80 if !ok { 81 return nil, fmt.Errorf("missing receipt root block: %s", root) 82 } 83 84 rmdl := rdm.ReceiptModel[O, X]{} 85 err = block.Decode(rblock, &rmdl, typ, cbor.Codec, sha256.Hasher, opts...) 86 if err != nil { 87 return nil, fmt.Errorf("decoding receipt: %w", err) 88 } 89 90 rcpt := receipt[O, X]{ 91 rt: rblock, 92 blks: blocks, 93 data: &rmdl, 94 } 95 96 return &rcpt, nil 97 } 98 99 func NewAnyReceipt(root ipld.Link, blocks blockstore.BlockReader, opts ...bindnode.Option) (AnyReceipt, error) { 100 anyReceiptType := rdm.TypeSystem().TypeByName("Receipt") 101 return NewReceipt[ipld.Node, ipld.Node](root, blocks, anyReceiptType, opts...) 102 } 103 104 func (r *receipt[O, X]) Blocks() iter.Seq2[block.Block, error] { 105 return r.blks.Iterator() 106 } 107 108 func (r *receipt[O, X]) Fx() fx.Effects { 109 var fork []fx.Effect 110 var join fx.Effect 111 for _, l := range r.data.Ocm.Fx.Fork { 112 b, _, _ := r.blks.Get(l) 113 if b != nil { 114 inv, _ := delegation.NewDelegation(b, r.blks) 115 fork = append(fork, fx.FromInvocation(inv)) 116 } else { 117 fork = append(fork, fx.FromLink(l)) 118 } 119 } 120 121 if r.data.Ocm.Fx.Join != nil { 122 b, _, _ := r.blks.Get(r.data.Ocm.Fx.Join) 123 if b != nil { 124 inv, _ := delegation.NewDelegation(b, r.blks) 125 join = fx.FromInvocation(inv) 126 } else { 127 join = fx.FromLink(r.data.Ocm.Fx.Join) 128 } 129 } 130 131 return fx.NewEffects(fx.WithFork(fork...), fx.WithJoin(join)) 132 } 133 134 func (r *receipt[O, X]) Issuer() ucan.Principal { 135 if r.data.Ocm.Iss == nil { 136 return nil 137 } 138 principal, err := did.Parse(*r.data.Ocm.Iss) 139 if err != nil { 140 fmt.Printf("Error: decoding issuer DID: %s\n", err) 141 } 142 return principal 143 } 144 145 func (r *receipt[O, X]) Proofs() delegation.Proofs { 146 return delegation.NewProofsView(r.data.Ocm.Prf, r.blks) 147 } 148 149 // Map values are datamodel.Node 150 func (r *receipt[O, X]) Meta() map[string]any { 151 meta := map[string]any{} 152 for k, v := range r.data.Ocm.Meta.Values { 153 meta[k] = any(v) 154 } 155 return meta 156 } 157 158 func (r *receipt[O, X]) Out() result.Result[O, X] { 159 return fromResultModel(r.data.Ocm.Out) 160 } 161 162 func (r *receipt[O, X]) Ran() ran.Ran { 163 _, ok, err := r.blks.Get(r.data.Ocm.Ran) 164 if !ok || err != nil { 165 return ran.FromLink(r.data.Ocm.Ran) 166 } 167 inv, err := invocation.NewInvocationView(r.data.Ocm.Ran, r.blks) 168 if err != nil { 169 fmt.Printf("Error: creating invocation view: %s\n", err) 170 return ran.FromLink(r.data.Ocm.Ran) 171 } 172 return ran.FromInvocation(inv) 173 } 174 175 func (r *receipt[O, X]) Root() block.Block { 176 return r.rt 177 } 178 179 func (r *receipt[O, X]) Signature() signature.SignatureView { 180 return signature.NewSignatureView(signature.Decode(r.data.Sig)) 181 } 182 183 func (r *receipt[O, X]) VerifySignature(verifier signature.Verifier) (bool, error) { 184 nodeResult, err := result.MapResultR1(r.Out(), toIPLDNode, toIPLDNode) 185 if err != nil { 186 return false, err 187 } 188 189 resultModel := toResultModel(nodeResult) 190 outcomeModel := rdm.OutcomeModel[ipld.Node, ipld.Node]{ 191 Ran: r.data.Ocm.Ran, 192 Out: resultModel, 193 Fx: r.data.Ocm.Fx, 194 Meta: r.data.Ocm.Meta, 195 Iss: r.data.Ocm.Iss, 196 Prf: r.data.Ocm.Prf, 197 } 198 outcomeBytes, err := cbor.Encode(&outcomeModel, rdm.TypeSystem().TypeByName("Outcome")) 199 if err != nil { 200 return false, err 201 } 202 203 return r.Signature().Verify(outcomeBytes, verifier), nil 204 } 205 206 func toIPLDNode[T any](b T) (ipld.Node, error) { 207 switch b := any(b).(type) { 208 case ipld.Node: 209 return b, nil 210 case ipld.Builder: 211 return b.ToIPLD() 212 default: 213 return nil, fmt.Errorf("expected IPLD node or builder, got %T", b) 214 } 215 } 216 217 func (r *receipt[O, X]) Archive() io.Reader { 218 // We create a descriptor block to describe what this DAG represents 219 variant, err := block.Encode( 220 &rdm.ArchiveModel{UcanReceipt0_9_1: r.rt.Link()}, 221 rdm.ArchiveType(), 222 cbor.Codec, 223 sha256.Hasher, 224 ) 225 if err != nil { 226 reader, _ := io.Pipe() 227 reader.CloseWithError(fmt.Errorf("hashing variant block bytes: %w", err)) 228 return reader 229 } 230 231 return car.Encode([]ipld.Link{variant.Link()}, func(yield func(ipld.Block, error) bool) { 232 for b, err := range r.Export() { 233 if !yield(b, err) || err != nil { 234 return 235 } 236 } 237 yield(variant, nil) 238 }) 239 } 240 241 // Export ONLY the blocks that comprise the receipt, its original invocation and its proofs 242 // This differs from Blocks(), which simply returns all the blocks in the backing blockstore 243 func (r *receipt[O, X]) Export() iter.Seq2[block.Block, error] { 244 var iterators []iter.Seq2[block.Block, error] 245 246 if inv, ok := r.Ran().Invocation(); ok { 247 iterators = append(iterators, inv.Export()) 248 } 249 250 for _, f := range r.Fx().Fork() { 251 if inv, ok := f.Invocation(); ok { 252 iterators = append(iterators, inv.Export()) 253 } 254 } 255 256 if inv, ok := r.Fx().Join().Invocation(); ok { 257 iterators = append(iterators, inv.Export()) 258 } 259 260 for _, prf := range r.Proofs() { 261 if delegation, ok := prf.Delegation(); ok { 262 iterators = append(iterators, delegation.Export()) 263 } 264 } 265 266 iterators = append(iterators, func(yield func(block.Block, error) bool) { yield(r.Root(), nil) }) 267 268 return iterable.Concat2(iterators...) 269 } 270 271 // Clone returns a new Receipt by copying r's backing blockstore. 272 func (r *receipt[O, X]) Clone() (Receipt[O, X], error) { 273 blks, err := blockstore.NewBlockStore(blockstore.WithBlocksIterator(r.blks.Iterator())) 274 if err != nil { 275 return nil, fmt.Errorf("creating block reader: %w", err) 276 } 277 return &receipt[O, X]{ 278 rt: r.rt, 279 blks: blks, 280 data: r.data, 281 }, nil 282 } 283 284 // AttachInvocation adds the invocation's blocks to the receipt's blockstore. 285 // If r already has an invocation, it returns r unchanged. 286 // If the invocation doesn't match r's ran, it returns an error. 287 func (r *receipt[O, X]) AttachInvocation(invocation invocation.Invocation) error { 288 // confirm the invocation matches the receipt 289 ran := r.Ran() 290 if ran.Link().String() != invocation.Link().String() { 291 return fmt.Errorf("expected invocation with CID %s, got %s", ran.Link(), invocation.Link()) 292 } 293 294 // don't add the invocation if it's already there 295 if _, ok := ran.Invocation(); ok { 296 return nil 297 } 298 299 // no need to copy receipt blocks if the backing BlockReader is actually a BlockStore 300 if bs, ok := r.blks.(blockstore.BlockStore); ok { 301 for b, err := range invocation.Export() { 302 if err != nil { 303 return fmt.Errorf("attaching invocation blocks: %w", err) 304 } 305 bs.Put(b) 306 } 307 } else { 308 blks, err := blockstore.NewBlockReader(blockstore.WithBlocksIterator(iterable.Concat2(r.blks.Iterator(), invocation.Export()))) 309 if err != nil { 310 return fmt.Errorf("creating block reader: %w", err) 311 } 312 313 r.blks = blks 314 } 315 316 return nil 317 } 318 319 type ReceiptReader[O, X any] interface { 320 Read(rcpt ipld.Link, blks iter.Seq2[block.Block, error]) (Receipt[O, X], error) 321 } 322 323 type receiptReader[O, X any] struct { 324 typ schema.Type 325 opts []bindnode.Option 326 } 327 328 func (rr *receiptReader[O, X]) Read(rcpt ipld.Link, blks iter.Seq2[block.Block, error]) (Receipt[O, X], error) { 329 br, err := blockstore.NewBlockReader(blockstore.WithBlocksIterator(blks)) 330 if err != nil { 331 return nil, fmt.Errorf("creating block reader: %w", err) 332 } 333 return NewReceipt[O, X](rcpt, br, rr.typ, rr.opts...) 334 } 335 336 func NewReceiptReader[O, X any](resultschema []byte, opts ...bindnode.Option) (ReceiptReader[O, X], error) { 337 typ, err := rdm.NewReceiptModelType(resultschema) 338 if err != nil { 339 return nil, fmt.Errorf("loading receipt data model: %w", err) 340 } 341 return &receiptReader[O, X]{typ, opts}, nil 342 } 343 344 func NewAnyReceiptReader(opts ...bindnode.Option) ReceiptReader[ipld.Node, ipld.Node] { 345 anyReceiptType := rdm.TypeSystem().TypeByName("Receipt") 346 return &receiptReader[ipld.Node, ipld.Node]{anyReceiptType, opts} 347 } 348 349 func NewReceiptReaderFromTypes[O, X any](successType schema.Type, errType schema.Type, opts ...bindnode.Option) (ReceiptReader[O, X], error) { 350 typ, err := rdm.NewReceiptModelFromTypes(successType, errType) 351 if err != nil { 352 return nil, fmt.Errorf("loading receipt data model: %w", err) 353 } 354 return &receiptReader[O, X]{typ, opts}, nil 355 } 356 357 type AnyReceipt Receipt[ipld.Node, ipld.Node] 358 359 func Rebind[O, X any](from AnyReceipt, successType schema.Type, errorType schema.Type, opts ...bindnode.Option) (Receipt[O, X], error) { 360 rdr, err := NewReceiptReaderFromTypes[O, X](successType, errorType, opts...) 361 if err != nil { 362 return nil, err 363 } 364 return rdr.Read(from.Root().Link(), from.Blocks()) 365 } 366 367 func Extract(b []byte) (AnyReceipt, error) { 368 roots, blks, err := car.Decode(bytes.NewReader(b)) 369 if err != nil { 370 return nil, fmt.Errorf("decoding CAR: %s", err) 371 } 372 if len(roots) == 0 { 373 return nil, fmt.Errorf("missing root CID in receipt archive") 374 } 375 if len(roots) > 1 { 376 return nil, fmt.Errorf("unexpected number of root CIDs in archive: %d", len(roots)) 377 } 378 379 br, err := blockstore.NewBlockReader(blockstore.WithBlocksIterator(blks)) 380 if err != nil { 381 return nil, fmt.Errorf("creating block reader: %w", err) 382 } 383 384 rt, ok, err := br.Get(roots[0]) 385 if err != nil { 386 return nil, fmt.Errorf("getting root block: %w", err) 387 } 388 if !ok { 389 return nil, fmt.Errorf("missing root block: %s", roots[0]) 390 } 391 392 model := rdm.ArchiveModel{} 393 err = block.Decode( 394 rt, 395 &model, 396 rdm.ArchiveType(), 397 cbor.Codec, 398 sha256.Hasher, 399 ) 400 if err != nil { 401 return nil, fmt.Errorf("decoding root block: %w", err) 402 } 403 404 return NewAnyReceipt(model.UcanReceipt0_9_1, br) 405 } 406 407 // Option is an option configuring a UCAN delegation. 408 type Option func(cfg *receiptConfig) error 409 410 type receiptConfig struct { 411 meta map[string]any 412 prf delegation.Proofs 413 forks []fx.Effect 414 join fx.Effect 415 } 416 417 // WithProofs configures the proofs for the receipt. If the `issuer` of this 418 // `Receipt` is not the resource owner / service provider, for the delegated 419 // capabilities, the `proofs` must contain valid `Proof`s containing 420 // delegations to the `issuer`. 421 func WithProofs(prf delegation.Proofs) Option { 422 return func(cfg *receiptConfig) error { 423 cfg.prf = prf 424 return nil 425 } 426 } 427 428 // WithMeta configures the metadata for the receipt. 429 func WithMeta(meta map[string]any) Option { 430 return func(cfg *receiptConfig) error { 431 cfg.meta = meta 432 return nil 433 } 434 } 435 436 // WithFork configures the forks for the receipt. 437 func WithFork(forks ...fx.Effect) Option { 438 return func(cfg *receiptConfig) error { 439 cfg.forks = forks 440 return nil 441 } 442 } 443 444 // WithJoin configures the join for the receipt. 445 func WithJoin(join fx.Effect) Option { 446 return func(cfg *receiptConfig) error { 447 cfg.join = join 448 return nil 449 } 450 } 451 452 func Issue[O, X ipld.Builder](issuer ucan.Signer, out result.Result[O, X], ran ran.Ran, opts ...Option) (AnyReceipt, error) { 453 cfg := receiptConfig{} 454 for _, opt := range opts { 455 if err := opt(&cfg); err != nil { 456 return nil, err 457 } 458 } 459 460 bs, err := blockstore.NewBlockStore() 461 if err != nil { 462 return nil, err 463 } 464 465 // copy invocation blocks into the store 466 invocationLink, err := ran.WriteInto(bs) 467 if err != nil { 468 return nil, err 469 } 470 471 var forks []ipld.Link 472 for _, effect := range cfg.forks { 473 if inv, ok := effect.Invocation(); ok { 474 blockstore.WriteInto(inv, bs) 475 } 476 forks = append(forks, effect.Link()) 477 } 478 479 var join ipld.Link 480 if cfg.join != (fx.Effect{}) { 481 if inv, ok := cfg.join.Invocation(); ok { 482 blockstore.WriteInto(inv, bs) 483 } 484 join = cfg.join.Link() 485 } 486 487 effectsModel := rdm.EffectsModel{ 488 Fork: forks, 489 Join: join, 490 } 491 492 // copy proof blocks into store 493 prooflinks, err := cfg.prf.WriteInto(bs) 494 if err != nil { 495 return nil, err 496 } 497 498 metaModel := rdm.MetaModel{} 499 // attempt to convert meta into IPLD format if present. 500 if cfg.meta != nil { 501 metaModel.Values = make(map[string]datamodel.Node, len(cfg.meta)) 502 for k, v := range cfg.meta { 503 nd, err := ipld.WrapWithRecovery(v, nil) 504 if err != nil { 505 return nil, err 506 } 507 metaModel.Keys = append(metaModel.Keys, k) 508 metaModel.Values[k] = nd 509 } 510 } 511 512 nodeResult, err := result.MapResultR1(out, func(b O) (ipld.Node, error) { 513 return b.ToIPLD() 514 }, func(b X) (ipld.Node, error) { 515 return b.ToIPLD() 516 }) 517 if err != nil { 518 return nil, err 519 } 520 resultModel := toResultModel(nodeResult) 521 issString := issuer.DID().String() 522 outcomeModel := rdm.OutcomeModel[ipld.Node, ipld.Node]{ 523 Ran: invocationLink, 524 Out: resultModel, 525 Fx: effectsModel, 526 Iss: &issString, 527 Meta: metaModel, 528 Prf: prooflinks, 529 } 530 531 outcomeBytes, err := cbor.Encode(&outcomeModel, rdm.TypeSystem().TypeByName("Outcome")) 532 if err != nil { 533 return nil, err 534 } 535 signature := issuer.Sign(outcomeBytes).Bytes() 536 537 receiptModel := rdm.ReceiptModel[ipld.Node, ipld.Node]{ 538 Ocm: outcomeModel, 539 Sig: signature, 540 } 541 542 rt, err := block.Encode(&receiptModel, rdm.TypeSystem().TypeByName("Receipt"), cbor.Codec, sha256.Hasher) 543 if err != nil { 544 return nil, fmt.Errorf("encoding receipt: %w", err) 545 } 546 547 err = bs.Put(rt) 548 if err != nil { 549 return nil, fmt.Errorf("adding receipt root to store: %w", err) 550 } 551 552 return &receipt[ipld.Node, ipld.Node]{ 553 rt: rt, 554 blks: bs, 555 data: &receiptModel, 556 }, nil 557 }