From e41e1175d06f026f14c8d7e7da7c1502206c13c6 Mon Sep 17 00:00:00 2001 From: hannahhoward Date: Sun, 28 Jul 2024 11:23:31 -0700 Subject: [PATCH 1/3] feat(receipt): add receipt creation adds support for issueing receipts. Also increases API compatbility with UCanto. --- core/dag/blockstore/blockstore.go | 18 +++ core/delegation/delegate.go | 25 +-- core/delegation/delegation.go | 2 +- core/delegation/proofs.go | 60 ++++++++ core/invocation/invocation.go | 31 ++++ core/ipld/lib.go | 1 + core/ipld/view.go | 13 +- core/message/message.go | 2 +- core/receipt/datamodel/receipt.go | 4 +- core/receipt/datamodel/receipt_test.go | 16 +- core/receipt/receipt.go | 203 ++++++++++++++++++++++--- core/result/datamodel/failure.go | 42 +++++ core/result/datamodel/failure.ipldsch | 5 + core/result/result.go | 124 ++++++++++++--- go.mod | 1 + go.sum | 3 +- 16 files changed, 480 insertions(+), 70 deletions(-) create mode 100644 core/delegation/proofs.go create mode 100644 core/result/datamodel/failure.go create mode 100644 core/result/datamodel/failure.ipldsch diff --git a/core/dag/blockstore/blockstore.go b/core/dag/blockstore/blockstore.go index 9cc3f3e..0a4a4bf 100644 --- a/core/dag/blockstore/blockstore.go +++ b/core/dag/blockstore/blockstore.go @@ -194,3 +194,21 @@ func NewBlockReader(options ...Option) (BlockReader, error) { return &blockreader{keys, blks}, nil } + +func Encode(view ipld.View, bs BlockWriter) error { + blks := view.Blocks() + for { + b, err := blks.Next() + if err != nil { + if err == io.EOF { + break + } + return fmt.Errorf("reading proof blocks: %s", err) + } + err = bs.Put(b) + if err != nil { + return fmt.Errorf("putting proof block: %s", err) + } + } + return nil +} diff --git a/core/delegation/delegate.go b/core/delegation/delegate.go index 39fcdde..476bbfe 100644 --- a/core/delegation/delegate.go +++ b/core/delegation/delegate.go @@ -2,7 +2,6 @@ package delegation import ( "fmt" - "io" "github.com/web3-storage/go-ucanto/core/dag/blockstore" "github.com/web3-storage/go-ucanto/core/ipld/block" @@ -20,7 +19,7 @@ type delegationConfig struct { nbf uint64 nnc string fct []ucan.FactBuilder - prf []Delegation + prf Proofs } // WithExpiration configures the expiration time in UTC seconds since Unix @@ -61,7 +60,7 @@ func WithFacts(fct []ucan.FactBuilder) Option { // `Delegation` is not the resource owner / service provider, for the delegated // capabilities, the `proofs` must contain valid `Proof`s containing // delegations to the `issuer`. -func WithProofs(prf []Delegation) Option { +func WithProofs(prf Proofs) Option { return func(cfg *delegationConfig) error { cfg.prf = prf return nil @@ -79,28 +78,14 @@ func Delegate(issuer ucan.Signer, audience ucan.Principal, capabilities []ucan.C } } - var links []ucan.Link bs, err := blockstore.NewBlockStore() if err != nil { return nil, err } - for _, p := range cfg.prf { - links = append(links, p.Link()) - blks := p.Blocks() - for { - b, err := blks.Next() - if err != nil { - if err == io.EOF { - break - } - return nil, fmt.Errorf("reading proof blocks: %s", err) - } - err = bs.Put(b) - if err != nil { - return nil, fmt.Errorf("putting proof block: %s", err) - } - } + links, err := cfg.prf.Encode(bs) + if err != nil { + return nil, err } data, err := ucan.Issue( diff --git a/core/delegation/delegation.go b/core/delegation/delegation.go index 48780b0..283531c 100644 --- a/core/delegation/delegation.go +++ b/core/delegation/delegation.go @@ -22,7 +22,7 @@ import ( // Delagation is a materialized view of a UCAN delegation, which can be encoded // into a UCAN token and used as proof for an invocation or further delegations. type Delegation interface { - ipld.IPLDView + ipld.View // Link returns the IPLD link of the root block of the delegation. Link() ucan.Link // Archive writes the delegation to a Content Addressed aRchive (CAR). diff --git a/core/delegation/proofs.go b/core/delegation/proofs.go new file mode 100644 index 0000000..d51fc32 --- /dev/null +++ b/core/delegation/proofs.go @@ -0,0 +1,60 @@ +package delegation + +import ( + "github.com/web3-storage/go-ucanto/core/dag/blockstore" + "github.com/web3-storage/go-ucanto/core/ipld" + "github.com/web3-storage/go-ucanto/ucan" +) + +type Proof struct { + delegation Delegation + link ucan.Link +} + +func (p Proof) Delegation() (Delegation, bool) { + return p.delegation, p.delegation != nil +} + +func (p Proof) Link() ucan.Link { + if p.delegation != nil { + return p.delegation.Link() + } + return p.link +} + +func FromDelegation(delegation Delegation) Proof { + return Proof{delegation, nil} +} + +func FromLink(link ucan.Link) Proof { + return Proof{nil, link} +} + +type Proofs []Proof + +func NewProofsView(links []ipld.Link, bs blockstore.BlockReader) Proofs { + proofs := make(Proofs, 0, len(links)) + for _, link := range links { + if delegation, err := NewDelegationView(link, bs); err == nil { + proofs = append(proofs, FromDelegation(delegation)) + } else { + proofs = append(proofs, FromLink(link)) + } + } + return proofs +} + +// Encode writes a set of proofs, some of which may be full delegations to a blockstore +func (proofs Proofs) Encode(bs blockstore.BlockWriter) ([]ipld.Link, error) { + links := make([]ucan.Link, 0, len(proofs)) + for _, p := range proofs { + links = append(links, p.Link()) + if delegation, isDelegation := p.Delegation(); isDelegation { + err := blockstore.Encode(delegation, bs) + if err != nil { + return nil, err + } + } + } + return links, nil +} diff --git a/core/invocation/invocation.go b/core/invocation/invocation.go index 9ca4ff7..332260a 100644 --- a/core/invocation/invocation.go +++ b/core/invocation/invocation.go @@ -35,3 +35,34 @@ type IssuedInvocation interface { func Invoke(issuer ucan.Signer, audience ucan.Principal, capability ucan.Capability[ucan.CaveatBuilder], options ...delegation.Option) (IssuedInvocation, error) { return delegation.Delegate(issuer, audience, []ucan.Capability[ucan.CaveatBuilder]{capability}, options...) } + +type Ran struct { + invocation Invocation + link ucan.Link +} + +func (r Ran) Invocation() (Invocation, bool) { + return r.invocation, r.invocation != nil +} + +func (r Ran) Link() ucan.Link { + if r.invocation != nil { + return r.invocation.Link() + } + return r.link +} + +func FromInvocation(invocation Invocation) Ran { + return Ran{invocation, nil} +} + +func FromLink(link ucan.Link) Ran { + return Ran{nil, link} +} + +func (r Ran) Encode(bs blockstore.BlockWriter) (ipld.Link, error) { + if invocation, ok := r.Invocation(); ok { + return r.Link(), blockstore.Encode(invocation, bs) + } + return r.Link(), nil +} diff --git a/core/ipld/lib.go b/core/ipld/lib.go index 900cdc0..06ccb1a 100644 --- a/core/ipld/lib.go +++ b/core/ipld/lib.go @@ -7,3 +7,4 @@ import ( type Link = ipld.Link type Block = block.Block +type Node = ipld.Node diff --git a/core/ipld/view.go b/core/ipld/view.go index 314c72d..da9026b 100644 --- a/core/ipld/view.go +++ b/core/ipld/view.go @@ -7,7 +7,7 @@ import ( // View represents a materialized IPLD DAG View, which provides a generic // traversal API. It is useful for encoding (potentially partial) IPLD DAGs // into content archives (e.g. CARs). -type IPLDView interface { +type View interface { // Root is the root block of the IPLD DAG this is the view of. This is the // block from which all other blocks are linked directly or transitively. Root() Block @@ -21,3 +21,14 @@ type IPLDView interface { // omitting it when encoding the view into a CAR archive. Blocks() iterable.Iterator[Block] } + +// ViewBuilder represents a materializable IPLD DAG View. It is a useful +// abstraction that can be used to defer actual IPLD encoding. +// +// Note that represented DAG could be partial implying that some of the blocks +// may not be included. This by design allowing a user to include whatever +// blocks they want to include. +type ViewBuilder[V View] interface { + // BuildIPLDView encodes all the blocks and creates a new IPLDView instance over them. + BuildIPLDView() V +} diff --git a/core/message/message.go b/core/message/message.go index 270a548..bf7cd9f 100644 --- a/core/message/message.go +++ b/core/message/message.go @@ -15,7 +15,7 @@ import ( ) type AgentMessage interface { - ipld.IPLDView + ipld.View // Invocations is a list of links to the root block of invocations than can // be found in the message. Invocations() []ipld.Link diff --git a/core/receipt/datamodel/receipt.go b/core/receipt/datamodel/receipt.go index efe185e..83d3187 100644 --- a/core/receipt/datamodel/receipt.go +++ b/core/receipt/datamodel/receipt.go @@ -37,8 +37,8 @@ type MetaModel struct { } type ResultModel[O any, X any] struct { - Ok O - Err X + Ok *O + Err *X } // NewReceiptModelType creates a new schema.Type for a Receipt. You must diff --git a/core/receipt/datamodel/receipt_test.go b/core/receipt/datamodel/receipt_test.go index 2e526ef..2c99358 100644 --- a/core/receipt/datamodel/receipt_test.go +++ b/core/receipt/datamodel/receipt_test.go @@ -39,10 +39,10 @@ func TestEncodeDecode(t *testing.T) { } l := cidlink.Link{Cid: cid.MustParse("bafkreiem4twkqzsq2aj4shbycd4yvoj2cx72vezicletlhi7dijjciqpui")} - r0 := rdm.ReceiptModel[*resultOk, *resultErr]{ - Ocm: rdm.OutcomeModel[*resultOk, *resultErr]{ + r0 := rdm.ReceiptModel[resultOk, resultErr]{ + Ocm: rdm.OutcomeModel[resultOk, resultErr]{ Ran: l, - Out: rdm.ResultModel[*resultOk, *resultErr]{ + Out: rdm.ResultModel[resultOk, resultErr]{ Ok: &resultOk{Status: "done"}, }, }, @@ -51,7 +51,7 @@ func TestEncodeDecode(t *testing.T) { if err != nil { t.Fatalf("encoding receipt: %s", err) } - r1 := rdm.ReceiptModel[*resultOk, *resultErr]{} + r1 := rdm.ReceiptModel[resultOk, resultErr]{} err = block.Decode(b0, &r1, typ, cbor.Codec, sha256.Hasher) if err != nil { t.Fatalf("decoding receipt: %s", err) @@ -63,10 +63,10 @@ func TestEncodeDecode(t *testing.T) { t.Fatalf("status was not done") } - r2 := rdm.ReceiptModel[*resultOk, *resultErr]{ - Ocm: rdm.OutcomeModel[*resultOk, *resultErr]{ + r2 := rdm.ReceiptModel[resultOk, resultErr]{ + Ocm: rdm.OutcomeModel[resultOk, resultErr]{ Ran: l, - Out: rdm.ResultModel[*resultOk, *resultErr]{ + Out: rdm.ResultModel[resultOk, resultErr]{ Err: &resultErr{Message: "boom"}, }, }, @@ -75,7 +75,7 @@ func TestEncodeDecode(t *testing.T) { if err != nil { t.Fatalf("encoding receipt: %s", err) } - r3 := rdm.ReceiptModel[*resultOk, *resultErr]{} + r3 := rdm.ReceiptModel[resultOk, resultErr]{} err = block.Decode(b1, &r3, typ, cbor.Codec, sha256.Hasher) if err != nil { t.Fatalf("decoding receipt: %s", err) diff --git a/core/receipt/receipt.go b/core/receipt/receipt.go index 7ed6908..8303a20 100644 --- a/core/receipt/receipt.go +++ b/core/receipt/receipt.go @@ -3,6 +3,8 @@ package receipt import ( "fmt" + "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/node/bindnode" "github.com/ipld/go-ipld-prime/schema" "github.com/web3-storage/go-ucanto/core/dag/blockstore" "github.com/web3-storage/go-ucanto/core/delegation" @@ -28,13 +30,13 @@ type Effects interface { // an ergonomic API and allows you to reference linked IPLD objects of they are // included in the source DAG. type Receipt[O, X any] interface { - ipld.IPLDView + ipld.View Ran() invocation.Invocation Out() result.Result[O, X] Fx() Effects Meta() map[string]any Issuer() ucan.Principal - Proofs() []delegation.Delegation + Proofs() delegation.Proofs Signature() signature.SignatureView } @@ -42,12 +44,20 @@ type results[O, X any] struct { model *rdm.ResultModel[O, X] } -func (r results[O, X]) Error() X { - return r.model.Err +func (r results[O, X]) Error() (X, bool) { + if r.model.Err != nil { + return *r.model.Err, true + } + var x X + return x, false } -func (r results[O, X]) Ok() O { - return r.model.Ok +func (r results[O, X]) Ok() (O, bool) { + if r.model.Ok != nil { + return *r.model.Ok, true + } + var o O + return o, false } type effects struct { @@ -75,7 +85,9 @@ func (r *receipt[O, X]) Blocks() iterable.Iterator[block.Block] { iterators = append(iterators, r.Ran().Blocks()) for _, prf := range r.Proofs() { - iterators = append(iterators, prf.Blocks()) + if delegation, ok := prf.Delegation(); ok { + iterators = append(iterators, delegation.Blocks()) + } } iterators = append(iterators, iterable.From([]block.Block{r.Root()})) @@ -98,17 +110,8 @@ func (r *receipt[O, X]) Issuer() ucan.Principal { return principal } -func (r *receipt[O, X]) Proofs() []delegation.Delegation { - var proofs []delegation.Delegation - for _, link := range r.data.Ocm.Prf { - prf, err := delegation.NewDelegationView(link, r.blks) - if err != nil { - fmt.Printf("Error: creating delegation view: %s\n", err) - continue - } - proofs = append(proofs, prf) - } - return proofs +func (r *receipt[O, X]) Proofs() delegation.Proofs { + return delegation.NewProofsView(r.data.Ocm.Prf, r.blks) } // Map values are datamodel.Node @@ -187,3 +190,167 @@ func NewReceiptReader[O, X any](resultschema []byte) (ReceiptReader[O, X], error } return &receiptReader[O, X]{typ}, nil } + +type UniversalReceipt Receipt[datamodel.Node, datamodel.Node] + +var ( + universalReceiptTs *schema.TypeSystem +) + +func init() { + ts, err := rdm.NewReceiptModelType( + []byte(`type Result union { + | any "ok" + | any "error" + } representation keyed + `)) + if err != nil { + panic(fmt.Errorf("failed to load IPLD schema: %s", err)) + } + universalReceiptTs = ts.TypeSystem() +} + +// Option is an option configuring a UCAN delegation. +type Option func(cfg *receiptConfig) error + +type receiptConfig struct { + meta map[string]any + prf delegation.Proofs + forks []ipld.Link + join ipld.Link +} + +// WithProofs configures the proofs for the receipt. If the `issuer` of this +// `Receipt` is not the resource owner / service provider, for the delegated +// capabilities, the `proofs` must contain valid `Proof`s containing +// delegations to the `issuer`. +func WithProofs(prf delegation.Proofs) Option { + return func(cfg *receiptConfig) error { + cfg.prf = prf + return nil + } +} + +// WithMeta configures the metadata for the receipt. +func WithMeta(meta map[string]any) Option { + return func(cfg *receiptConfig) error { + cfg.meta = meta + return nil + } +} + +// WithForks configures the forks for the receipt. +func WithForks(forks []ipld.Link) Option { + return func(cfg *receiptConfig) error { + cfg.forks = forks + return nil + } +} + +// WithJoin configures the join for the receipt. +func WithJoin(join ipld.Link) Option { + return func(cfg *receiptConfig) error { + cfg.join = join + return nil + } +} + +func wrapOrFail(value interface{}) (nd schema.TypedNode, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("%v", r) + } + }() + nd = bindnode.Wrap(value, nil) + return +} + +func Issue(issuer ucan.Signer, result result.UniversalResult, ran invocation.Ran, opts ...Option) (UniversalReceipt, error) { + cfg := receiptConfig{} + for _, opt := range opts { + if err := opt(&cfg); err != nil { + return nil, err + } + } + + bs, err := blockstore.NewBlockStore() + if err != nil { + return nil, err + } + + // copy invocation blocks into the store + invocationLink, err := ran.Encode(bs) + if err != nil { + return nil, err + } + + // copy proof blocks into store + prooflinks, err := cfg.prf.Encode(bs) + if err != nil { + return nil, err + } + + effectsModel := rdm.EffectsModel{ + Fork: cfg.forks, + Join: cfg.join, + } + + metaModel := rdm.MetaModel{} + // attempt to convert meta into IPLD format if present. + if cfg.meta != nil { + metaModel.Values = make(map[string]datamodel.Node, len(cfg.meta)) + for k, v := range cfg.meta { + nd, err := wrapOrFail(v) + if err != nil { + return nil, err + } + metaModel.Keys = append(metaModel.Keys, k) + metaModel.Values[k] = nd + } + } + + resultModel := rdm.ResultModel[datamodel.Node, datamodel.Node]{} + if success, ok := result.Ok(); ok { + resultModel.Ok = &success + } + if err, ok := result.Error(); ok { + resultModel.Err = &err + } + + issString := issuer.DID().String() + outcomeModel := rdm.OutcomeModel[datamodel.Node, datamodel.Node]{ + Ran: invocationLink, + Out: resultModel, + Fx: effectsModel, + Iss: &issString, + Meta: metaModel, + Prf: prooflinks, + } + + outcomeBytes, err := cbor.Encode(outcomeModel, universalReceiptTs.TypeByName("Outcome")) + if err != nil { + return nil, err + } + signature := issuer.Sign(outcomeBytes).Bytes() + + receiptModel := rdm.ReceiptModel[datamodel.Node, datamodel.Node]{ + Ocm: outcomeModel, + Sig: signature, + } + + rt, err := block.Encode(receiptModel, universalReceiptTs.TypeByName("Receipt"), cbor.Codec, sha256.Hasher) + if err != nil { + return nil, fmt.Errorf("encoding UCAN: %s", err) + } + + err = bs.Put(rt) + if err != nil { + return nil, fmt.Errorf("adding delegation root to store: %s", err) + } + + return &receipt[datamodel.Node, datamodel.Node]{ + rt: rt, + blks: bs, + data: &receiptModel, + }, nil +} diff --git a/core/result/datamodel/failure.go b/core/result/datamodel/failure.go new file mode 100644 index 0000000..e573b38 --- /dev/null +++ b/core/result/datamodel/failure.go @@ -0,0 +1,42 @@ +package datamodel + +import ( + // to use go:embed + _ "embed" + "fmt" + "sync" + + "github.com/ipld/go-ipld-prime" + "github.com/ipld/go-ipld-prime/schema" +) + +//go:embed failure.ipldsch +var failureSchema []byte + +// Failure is a generic failure +type Failure struct { + Name *string + Message string + Stack *string +} + +var ( + once sync.Once + ts *schema.TypeSystem + err error +) + +func mustLoadSchema() *schema.TypeSystem { + once.Do(func() { + ts, err = ipld.LoadSchemaBytes(failureSchema) + }) + if err != nil { + panic(fmt.Errorf("failed to load IPLD schema: %s", err)) + } + return ts +} + +// returns the failure schematype +func Type() schema.Type { + return mustLoadSchema().TypeByName("Failure") +} diff --git a/core/result/datamodel/failure.ipldsch b/core/result/datamodel/failure.ipldsch new file mode 100644 index 0000000..243f336 --- /dev/null +++ b/core/result/datamodel/failure.ipldsch @@ -0,0 +1,5 @@ +type Failure struct { + name optional String + message String + stack optional String +} \ No newline at end of file diff --git a/core/result/result.go b/core/result/result.go index 94f5e3f..6d1f7aa 100644 --- a/core/result/result.go +++ b/core/result/result.go @@ -1,28 +1,116 @@ package result +import ( + "fmt" + "runtime" + + "github.com/ipld/go-ipld-prime/node/bindnode" + "github.com/pkg/errors" + "github.com/web3-storage/go-ucanto/core/ipld" + "github.com/web3-storage/go-ucanto/core/result/datamodel" +) + // https://github.com/ucan-wg/invocation/#6-result type Result[O any, X any] interface { - Ok() O - Error() X + Ok() (O, bool) + Error() (X, bool) +} + +type UniversalResult Result[ipld.Node, ipld.Node] + +type universalResult struct { + ok *ipld.Node + err *ipld.Node +} + +func (ur universalResult) Ok() (ipld.Node, bool) { + if ur.ok != nil { + return *ur.ok, true + } + return nil, false +} + +func (ur universalResult) Error() (ipld.Node, bool) { + if ur.err != nil { + return *ur.err, true + } + return nil, false +} + +func Ok(value ipld.Node) UniversalResult { + return universalResult{&value, nil} +} + +func Error(err ipld.Node) UniversalResult { + return universalResult{nil, &err} +} + +// Named is an error that you can read a name from +type Named interface { + Name() string } -// type result[O any, X any] struct { -// ok O -// err X -// } +// WithStackTrace is an error that you can read a stack trace from +type WithStackTrace interface { + Stack() string +} + +// IPLDConvertableError is an error with a custom method to convert to an IPLD Node +type IPLDConvertableError interface { + error + ToIPLD() ipld.Node +} + +type NamedWithStackTrace interface { + Named + WithStackTrace +} -// func (r result[O, X]) Ok() O { -// return r.ok -// } +type namedWithStackTrace struct { + name string + stack errors.StackTrace +} -// func (r result[O, X]) Error() X { -// return r.err -// } +func (n namedWithStackTrace) Name() string { + return n.name +} -// func Ok[O any](value O) Result[O, any] { -// return result[O, any]{value, nil} -// } +func (n namedWithStackTrace) Stack() string { + return fmt.Sprintf("%+v", n.stack) +} -// func Error[X any](value X) Result[any, X] { -// return result[any, X]{nil, value} -// } +func NamedWithCurrentStackTrace(name string) NamedWithStackTrace { + const depth = 32 + + var pcs [depth]uintptr + n := runtime.Callers(3, pcs[:]) + + f := make(errors.StackTrace, n) + for i := 0; i < n; i++ { + f[i] = errors.Frame(pcs[i]) + } + + return namedWithStackTrace{name, f} +} + +// Failure generates a Result from a golang error, using: +// 1. a custom conversion to IPLD if present +// 2. the golangs error message plus +// a. a name, if it is a named error +// b. a stack trace, if it has a stack trace +func Failure(err error) UniversalResult { + if ipldConvertableError, ok := err.(IPLDConvertableError); ok { + return Error(ipldConvertableError.ToIPLD()) + } + + failure := datamodel.Failure{Message: err.Error()} + if named, ok := err.(Named); ok { + name := named.Name() + failure.Name = &name + } + if withStackTrace, ok := err.(WithStackTrace); ok { + stack := withStackTrace.Stack() + failure.Stack = &stack + } + return Error(bindnode.Wrap(&failure, datamodel.Type())) +} diff --git a/go.mod b/go.mod index dd336a2..8979b93 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/multiformats/go-multibase v0.2.0 github.com/multiformats/go-multihash v0.2.3 github.com/multiformats/go-varint v0.0.7 + github.com/pkg/errors v0.9.1 ) require ( diff --git a/go.sum b/go.sum index 4b6c530..c412866 100644 --- a/go.sum +++ b/go.sum @@ -202,8 +202,9 @@ github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOEL github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4= From fa4cbd7e5a487bbd8d4361eae51e908f63074178 Mon Sep 17 00:00:00 2001 From: hannahhoward Date: Sun, 28 Jul 2024 12:31:06 -0700 Subject: [PATCH 2/3] feat(message): add receipt encoding in messages --- client/connection.go | 2 +- core/message/message.go | 34 ++++++++++++++++++++++------------ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/client/connection.go b/client/connection.go index 1e13664..6fa2530 100644 --- a/client/connection.go +++ b/client/connection.go @@ -80,7 +80,7 @@ type ExecutionResponse interface { } func Execute(invocations []invocation.Invocation, conn Connection) (ExecutionResponse, error) { - input, err := message.Build(invocations) + input, err := message.Build(invocations, nil) if err != nil { return nil, fmt.Errorf("building message: %s", err) } diff --git a/core/message/message.go b/core/message/message.go index bf7cd9f..9008a35 100644 --- a/core/message/message.go +++ b/core/message/message.go @@ -2,7 +2,6 @@ package message import ( "fmt" - "io" "github.com/web3-storage/go-ucanto/core/dag/blockstore" "github.com/web3-storage/go-ucanto/core/invocation" @@ -12,6 +11,7 @@ import ( "github.com/web3-storage/go-ucanto/core/ipld/hash/sha256" "github.com/web3-storage/go-ucanto/core/iterable" mdm "github.com/web3-storage/go-ucanto/core/message/datamodel" + "github.com/web3-storage/go-ucanto/core/receipt" ) type AgentMessage interface { @@ -73,7 +73,7 @@ func (m *message) Get(link ipld.Link) (ipld.Link, bool) { return rcpt, true } -func Build(invocations []invocation.Invocation) (AgentMessage, error) { +func Build(invocations []invocation.Invocation, receipts []receipt.UniversalReceipt) (AgentMessage, error) { bs, err := blockstore.NewBlockStore() if err != nil { return nil, err @@ -83,18 +83,27 @@ func Build(invocations []invocation.Invocation) (AgentMessage, error) { for _, inv := range invocations { ex = append(ex, inv.Link()) - blks := inv.Blocks() - for { - b, err := blks.Next() + err := blockstore.Encode(inv, bs) + if err != nil { + return nil, err + } + } + + var report *mdm.ReportModel + if len(receipts) > 0 { + report = &mdm.ReportModel{ + Keys: make([]string, 0, len(receipts)), + Values: make(map[string]ipld.Link, len(receipts)), + } + for _, receipt := range receipts { + err := blockstore.Encode(receipt, bs) if err != nil { - if err == io.EOF { - break - } - return nil, fmt.Errorf("reading invocation blocks: %s", err) + return nil, err } - err = bs.Put(b) - if err != nil { - return nil, fmt.Errorf("putting invocation block: %s", err) + + key := receipt.Ran().Link().String() + if _, ok := report.Values[key]; !ok { + report.Values[key] = receipt.Root().Link() } } } @@ -102,6 +111,7 @@ func Build(invocations []invocation.Invocation) (AgentMessage, error) { msg := mdm.AgentMessageModel{ UcantoMessage7: &mdm.DataModel{ Execute: ex, + Report: report, }, } From 3bfd389c37724fe52f91de5ac5e43076b20d8564 Mon Sep 17 00:00:00 2001 From: hannahhoward Date: Mon, 29 Jul 2024 19:16:42 -0700 Subject: [PATCH 3/3] refactor(receipt): respond to PR changes --- core/dag/blockstore/blockstore.go | 2 +- core/delegation/delegate.go | 2 +- core/delegation/proofs.go | 6 +++--- core/invocation/invocation.go | 4 ++-- core/message/message.go | 6 +++--- core/receipt/anyresult.ipldsch | 4 ++++ core/receipt/receipt.go | 28 ++++++++++++++-------------- core/result/result.go | 26 +++++++++++++------------- 8 files changed, 41 insertions(+), 37 deletions(-) create mode 100644 core/receipt/anyresult.ipldsch diff --git a/core/dag/blockstore/blockstore.go b/core/dag/blockstore/blockstore.go index 0a4a4bf..bfb6a91 100644 --- a/core/dag/blockstore/blockstore.go +++ b/core/dag/blockstore/blockstore.go @@ -195,7 +195,7 @@ func NewBlockReader(options ...Option) (BlockReader, error) { return &blockreader{keys, blks}, nil } -func Encode(view ipld.View, bs BlockWriter) error { +func WriteInto(view ipld.View, bs BlockWriter) error { blks := view.Blocks() for { b, err := blks.Next() diff --git a/core/delegation/delegate.go b/core/delegation/delegate.go index 476bbfe..13a18a5 100644 --- a/core/delegation/delegate.go +++ b/core/delegation/delegate.go @@ -83,7 +83,7 @@ func Delegate(issuer ucan.Signer, audience ucan.Principal, capabilities []ucan.C return nil, err } - links, err := cfg.prf.Encode(bs) + links, err := cfg.prf.WriteInto(bs) if err != nil { return nil, err } diff --git a/core/delegation/proofs.go b/core/delegation/proofs.go index d51fc32..ae82e21 100644 --- a/core/delegation/proofs.go +++ b/core/delegation/proofs.go @@ -44,13 +44,13 @@ func NewProofsView(links []ipld.Link, bs blockstore.BlockReader) Proofs { return proofs } -// Encode writes a set of proofs, some of which may be full delegations to a blockstore -func (proofs Proofs) Encode(bs blockstore.BlockWriter) ([]ipld.Link, error) { +// WriteInto writes a set of proofs, some of which may be full delegations to a blockstore +func (proofs Proofs) WriteInto(bs blockstore.BlockWriter) ([]ipld.Link, error) { links := make([]ucan.Link, 0, len(proofs)) for _, p := range proofs { links = append(links, p.Link()) if delegation, isDelegation := p.Delegation(); isDelegation { - err := blockstore.Encode(delegation, bs) + err := blockstore.WriteInto(delegation, bs) if err != nil { return nil, err } diff --git a/core/invocation/invocation.go b/core/invocation/invocation.go index 332260a..0213a4f 100644 --- a/core/invocation/invocation.go +++ b/core/invocation/invocation.go @@ -60,9 +60,9 @@ func FromLink(link ucan.Link) Ran { return Ran{nil, link} } -func (r Ran) Encode(bs blockstore.BlockWriter) (ipld.Link, error) { +func (r Ran) WriteInto(bs blockstore.BlockWriter) (ipld.Link, error) { if invocation, ok := r.Invocation(); ok { - return r.Link(), blockstore.Encode(invocation, bs) + return r.Link(), blockstore.WriteInto(invocation, bs) } return r.Link(), nil } diff --git a/core/message/message.go b/core/message/message.go index 9008a35..a268bb9 100644 --- a/core/message/message.go +++ b/core/message/message.go @@ -73,7 +73,7 @@ func (m *message) Get(link ipld.Link) (ipld.Link, bool) { return rcpt, true } -func Build(invocations []invocation.Invocation, receipts []receipt.UniversalReceipt) (AgentMessage, error) { +func Build(invocations []invocation.Invocation, receipts []receipt.AnyReceipt) (AgentMessage, error) { bs, err := blockstore.NewBlockStore() if err != nil { return nil, err @@ -83,7 +83,7 @@ func Build(invocations []invocation.Invocation, receipts []receipt.UniversalRece for _, inv := range invocations { ex = append(ex, inv.Link()) - err := blockstore.Encode(inv, bs) + err := blockstore.WriteInto(inv, bs) if err != nil { return nil, err } @@ -96,7 +96,7 @@ func Build(invocations []invocation.Invocation, receipts []receipt.UniversalRece Values: make(map[string]ipld.Link, len(receipts)), } for _, receipt := range receipts { - err := blockstore.Encode(receipt, bs) + err := blockstore.WriteInto(receipt, bs) if err != nil { return nil, err } diff --git a/core/receipt/anyresult.ipldsch b/core/receipt/anyresult.ipldsch new file mode 100644 index 0000000..8f66737 --- /dev/null +++ b/core/receipt/anyresult.ipldsch @@ -0,0 +1,4 @@ +type Result union { + | any "ok" + | any "error" +} representation keyed diff --git a/core/receipt/receipt.go b/core/receipt/receipt.go index 8303a20..26f7743 100644 --- a/core/receipt/receipt.go +++ b/core/receipt/receipt.go @@ -1,6 +1,8 @@ package receipt import ( + // for go:embed + _ "embed" "fmt" "github.com/ipld/go-ipld-prime/datamodel" @@ -191,23 +193,21 @@ func NewReceiptReader[O, X any](resultschema []byte) (ReceiptReader[O, X], error return &receiptReader[O, X]{typ}, nil } -type UniversalReceipt Receipt[datamodel.Node, datamodel.Node] +type AnyReceipt Receipt[datamodel.Node, datamodel.Node] var ( - universalReceiptTs *schema.TypeSystem + anyReceiptTs *schema.TypeSystem ) +//go:embed anyresult.ipldsch +var anyResultSchema []byte + func init() { - ts, err := rdm.NewReceiptModelType( - []byte(`type Result union { - | any "ok" - | any "error" - } representation keyed - `)) + ts, err := rdm.NewReceiptModelType(anyResultSchema) if err != nil { panic(fmt.Errorf("failed to load IPLD schema: %s", err)) } - universalReceiptTs = ts.TypeSystem() + anyReceiptTs = ts.TypeSystem() } // Option is an option configuring a UCAN delegation. @@ -265,7 +265,7 @@ func wrapOrFail(value interface{}) (nd schema.TypedNode, err error) { return } -func Issue(issuer ucan.Signer, result result.UniversalResult, ran invocation.Ran, opts ...Option) (UniversalReceipt, error) { +func Issue(issuer ucan.Signer, result result.AnyResult, ran invocation.Ran, opts ...Option) (AnyReceipt, error) { cfg := receiptConfig{} for _, opt := range opts { if err := opt(&cfg); err != nil { @@ -279,13 +279,13 @@ func Issue(issuer ucan.Signer, result result.UniversalResult, ran invocation.Ran } // copy invocation blocks into the store - invocationLink, err := ran.Encode(bs) + invocationLink, err := ran.WriteInto(bs) if err != nil { return nil, err } // copy proof blocks into store - prooflinks, err := cfg.prf.Encode(bs) + prooflinks, err := cfg.prf.WriteInto(bs) if err != nil { return nil, err } @@ -327,7 +327,7 @@ func Issue(issuer ucan.Signer, result result.UniversalResult, ran invocation.Ran Prf: prooflinks, } - outcomeBytes, err := cbor.Encode(outcomeModel, universalReceiptTs.TypeByName("Outcome")) + outcomeBytes, err := cbor.Encode(outcomeModel, anyReceiptTs.TypeByName("Outcome")) if err != nil { return nil, err } @@ -338,7 +338,7 @@ func Issue(issuer ucan.Signer, result result.UniversalResult, ran invocation.Ran Sig: signature, } - rt, err := block.Encode(receiptModel, universalReceiptTs.TypeByName("Receipt"), cbor.Codec, sha256.Hasher) + rt, err := block.Encode(receiptModel, anyReceiptTs.TypeByName("Receipt"), cbor.Codec, sha256.Hasher) if err != nil { return nil, fmt.Errorf("encoding UCAN: %s", err) } diff --git a/core/result/result.go b/core/result/result.go index 6d1f7aa..ee3d8a1 100644 --- a/core/result/result.go +++ b/core/result/result.go @@ -16,33 +16,33 @@ type Result[O any, X any] interface { Error() (X, bool) } -type UniversalResult Result[ipld.Node, ipld.Node] +type AnyResult Result[ipld.Node, ipld.Node] -type universalResult struct { +type anyResult struct { ok *ipld.Node err *ipld.Node } -func (ur universalResult) Ok() (ipld.Node, bool) { - if ur.ok != nil { - return *ur.ok, true +func (ar anyResult) Ok() (ipld.Node, bool) { + if ar.ok != nil { + return *ar.ok, true } return nil, false } -func (ur universalResult) Error() (ipld.Node, bool) { - if ur.err != nil { - return *ur.err, true +func (ar anyResult) Error() (ipld.Node, bool) { + if ar.err != nil { + return *ar.err, true } return nil, false } -func Ok(value ipld.Node) UniversalResult { - return universalResult{&value, nil} +func Ok(value ipld.Node) AnyResult { + return anyResult{&value, nil} } -func Error(err ipld.Node) UniversalResult { - return universalResult{nil, &err} +func Error(err ipld.Node) AnyResult { + return anyResult{nil, &err} } // Named is an error that you can read a name from @@ -98,7 +98,7 @@ func NamedWithCurrentStackTrace(name string) NamedWithStackTrace { // 2. the golangs error message plus // a. a name, if it is a named error // b. a stack trace, if it has a stack trace -func Failure(err error) UniversalResult { +func Failure(err error) AnyResult { if ipldConvertableError, ok := err.(IPLDConvertableError); ok { return Error(ipldConvertableError.ToIPLD()) }