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  }