From a72237a4fdf9b6e5998de0288cbe902c9f1ed216 Mon Sep 17 00:00:00 2001 From: ash Date: Mon, 28 Oct 2024 16:39:16 +0000 Subject: [PATCH] feat: support invocations as effects (#30) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR allows completed effects to be included in a receipt. Similar to [`Ran`](https://github.com/storacha/go-ucanto/blob/main/core/invocation/ran/ran.go), an effect is now a union type - a link OR an invocation. This allows a location commitment to be returned from a `blob/accept` invocation. 🤔 Not sure I agree with using effects for this, but this is how it is done currently. --- core/receipt/fx/fx.go | 78 ++++++++++++++++++++++++++++ core/receipt/receipt.go | 76 +++++++++++++++++---------- core/receipt/receipt_test.go | 85 +++++++++++++++++++++++++++++++ server/handler.go | 4 +- server/server.go | 2 +- server/server_test.go | 7 +-- server/transaction/transaction.go | 30 +++-------- 7 files changed, 225 insertions(+), 57 deletions(-) create mode 100644 core/receipt/fx/fx.go create mode 100644 core/receipt/receipt_test.go diff --git a/core/receipt/fx/fx.go b/core/receipt/fx/fx.go new file mode 100644 index 0000000..d2cc53d --- /dev/null +++ b/core/receipt/fx/fx.go @@ -0,0 +1,78 @@ +package fx + +import ( + "github.com/storacha/go-ucanto/core/invocation" + "github.com/storacha/go-ucanto/ucan" +) + +type Effects interface { + Fork() []Effect + Join() Effect +} + +type effects struct { + fork []Effect + join Effect +} + +func (fx effects) Fork() []Effect { + return fx.fork +} + +func (fx effects) Join() Effect { + return fx.join +} + +// Option is an option configuring effects. +type Option func(fx *effects) error + +// WithFork configures the forks for the receipt. +func WithFork(forks ...Effect) Option { + return func(fx *effects) error { + fx.fork = forks + return nil + } +} + +// WithJoin configures the join for the receipt. +func WithJoin(join Effect) Option { + return func(fx *effects) error { + fx.join = join + return nil + } +} + +func NewEffects(opts ...Option) Effects { + var fx effects + for _, opt := range opts { + opt(&fx) + } + return fx +} + +// Effect is either an invocation or a link to one. +type Effect struct { + invocation invocation.Invocation + link ucan.Link +} + +// Invocation returns the invocation if it is available. +func (e Effect) Invocation() (invocation.Invocation, bool) { + return e.invocation, e.invocation != nil +} + +// Link returns the invocation root link. +func (e Effect) Link() ucan.Link { + if e.invocation != nil { + return e.invocation.Link() + } + return e.link +} + +func FromLink(link ucan.Link) Effect { + return Effect{nil, link} +} + +func FromInvocation(invocation invocation.Invocation) Effect { + return Effect{invocation, nil} +} diff --git a/core/receipt/receipt.go b/core/receipt/receipt.go index e13d52a..c6226dd 100644 --- a/core/receipt/receipt.go +++ b/core/receipt/receipt.go @@ -18,17 +18,13 @@ import ( "github.com/storacha/go-ucanto/core/ipld/hash/sha256" "github.com/storacha/go-ucanto/core/iterable" rdm "github.com/storacha/go-ucanto/core/receipt/datamodel" + "github.com/storacha/go-ucanto/core/receipt/fx" "github.com/storacha/go-ucanto/core/result" "github.com/storacha/go-ucanto/did" "github.com/storacha/go-ucanto/ucan" "github.com/storacha/go-ucanto/ucan/crypto/signature" ) -type Effects interface { - Fork() []ipld.Link - Join() ipld.Link -} - // Receipt represents a view of the invocation receipt. This interface provides // an ergonomic API and allows you to reference linked IPLD objects if they are // included in the source DAG. @@ -36,7 +32,7 @@ type Receipt[O, X any] interface { ipld.View Ran() invocation.Invocation Out() result.Result[O, X] - Fx() Effects + Fx() fx.Effects Meta() map[string]any Issuer() ucan.Principal Proofs() delegation.Proofs @@ -58,18 +54,6 @@ func fromResultModel[O, X any](resultModel rdm.ResultModel[O, X]) result.Result[ return result.Error[O, X](*resultModel.Err) } -type effects struct { - model rdm.EffectsModel -} - -func (fx effects) Fork() []ipld.Link { - return fx.model.Fork -} - -func (fx effects) Join() ipld.Link { - return fx.model.Join -} - type receipt[O, X any] struct { rt block.Block blks blockstore.BlockReader @@ -93,8 +77,30 @@ func (r *receipt[O, X]) Blocks() iter.Seq2[block.Block, error] { return iterable.Concat2(iterators...) } -func (r *receipt[O, X]) Fx() Effects { - return effects{r.data.Ocm.Fx} +func (r *receipt[O, X]) Fx() fx.Effects { + var fork []fx.Effect + var join fx.Effect + for _, l := range r.data.Ocm.Fx.Fork { + b, _, _ := r.blks.Get(l) + if b != nil { + inv, _ := delegation.NewDelegation(b, r.blks) + fork = append(fork, fx.FromInvocation(inv)) + } else { + fork = append(fork, fx.FromLink(l)) + } + } + + if r.data.Ocm.Fx.Join != nil { + b, _, _ := r.blks.Get(r.data.Ocm.Fx.Join) + if b != nil { + inv, _ := delegation.NewDelegation(b, r.blks) + join = fx.FromInvocation(inv) + } else { + join = fx.FromLink(r.data.Ocm.Fx.Join) + } + } + + return fx.NewEffects(fx.WithFork(fork...), fx.WithJoin(join)) } func (r *receipt[O, X]) Issuer() ucan.Principal { @@ -205,8 +211,8 @@ type Option func(cfg *receiptConfig) error type receiptConfig struct { meta map[string]any prf delegation.Proofs - forks []ipld.Link - join ipld.Link + forks []fx.Effect + join fx.Effect } // WithProofs configures the proofs for the receipt. If the `issuer` of this @@ -228,8 +234,8 @@ func WithMeta(meta map[string]any) Option { } } -// WithForks configures the forks for the receipt. -func WithForks(forks []ipld.Link) Option { +// WithFork configures the forks for the receipt. +func WithFork(forks ...fx.Effect) Option { return func(cfg *receiptConfig) error { cfg.forks = forks return nil @@ -237,7 +243,7 @@ func WithForks(forks []ipld.Link) Option { } // WithJoin configures the join for the receipt. -func WithJoin(join ipld.Link) Option { +func WithJoin(join fx.Effect) Option { return func(cfg *receiptConfig) error { cfg.join = join return nil @@ -269,9 +275,25 @@ func Issue[O, X ipld.Builder](issuer ucan.Signer, out result.Result[O, X], ran r return nil, err } + var forks []ipld.Link + for _, effect := range cfg.forks { + if inv, ok := effect.Invocation(); ok { + blockstore.WriteInto(inv, bs) + } + forks = append(forks, effect.Link()) + } + + var join ipld.Link + if cfg.join != (fx.Effect{}) { + if inv, ok := cfg.join.Invocation(); ok { + blockstore.WriteInto(inv, bs) + } + join = cfg.join.Link() + } + effectsModel := rdm.EffectsModel{ - Fork: cfg.forks, - Join: cfg.join, + Fork: forks, + Join: join, } metaModel := rdm.MetaModel{} diff --git a/core/receipt/receipt_test.go b/core/receipt/receipt_test.go new file mode 100644 index 0000000..338d397 --- /dev/null +++ b/core/receipt/receipt_test.go @@ -0,0 +1,85 @@ +package receipt + +import ( + "slices" + "testing" + + "github.com/storacha/go-ucanto/core/invocation" + "github.com/storacha/go-ucanto/core/invocation/ran" + "github.com/storacha/go-ucanto/core/ipld" + "github.com/storacha/go-ucanto/core/receipt/fx" + "github.com/storacha/go-ucanto/core/result" + "github.com/storacha/go-ucanto/core/result/ok" + "github.com/storacha/go-ucanto/testing/fixtures" + "github.com/storacha/go-ucanto/testing/helpers" + "github.com/storacha/go-ucanto/ucan" + "github.com/stretchr/testify/require" +) + +func TestEffects(t *testing.T) { + ran := ran.FromLink(helpers.RandomCID()) + out := result.Ok[ok.Unit, ipld.Builder](ok.Unit{}) + + t.Run("as links", func(t *testing.T) { + f0 := fx.FromLink(helpers.RandomCID()) + f1 := fx.FromLink(helpers.RandomCID()) + j := fx.FromLink(helpers.RandomCID()) + + receipt, err := Issue(fixtures.Alice, out, ran, WithFork(f0, f1), WithJoin(j)) + require.NoError(t, err) + + effects := receipt.Fx() + require.True(t, slices.ContainsFunc(effects.Fork(), func(f fx.Effect) bool { + return f.Link().String() == f0.Link().String() + })) + require.True(t, slices.ContainsFunc(effects.Fork(), func(f fx.Effect) bool { + return f.Link().String() == f1.Link().String() + })) + require.Equal(t, effects.Join().Link(), j.Link()) + }) + + t.Run("as invocations", func(t *testing.T) { + i0, err := invocation.Invoke( + fixtures.Alice, + fixtures.Bob, + ucan.NewCapability("fx/0", fixtures.Alice.DID().String(), ucan.NoCaveats{}), + ) + require.NoError(t, err) + i1, err := invocation.Invoke( + fixtures.Alice, + fixtures.Mallory, + ucan.NewCapability("fx/1", fixtures.Alice.DID().String(), ucan.NoCaveats{}), + ) + require.NoError(t, err) + i2, err := invocation.Invoke( + fixtures.Mallory, + fixtures.Bob, + ucan.NewCapability("fx/2", fixtures.Alice.DID().String(), ucan.NoCaveats{}), + ) + require.NoError(t, err) + + f0 := fx.FromInvocation(i0) + f1 := fx.FromInvocation(i1) + j := fx.FromInvocation(i2) + + receipt, err := Issue(fixtures.Alice, out, ran, WithFork(f0, f1), WithJoin(j)) + require.NoError(t, err) + + effects := receipt.Fx() + require.True(t, slices.ContainsFunc(effects.Fork(), func(f fx.Effect) bool { + return f.Link().String() == f0.Link().String() + })) + require.True(t, slices.ContainsFunc(effects.Fork(), func(f fx.Effect) bool { + return f.Link().String() == f1.Link().String() + })) + require.Equal(t, effects.Join().Link(), j.Link()) + + for _, effect := range effects.Fork() { + _, ok := effect.Invocation() + require.True(t, ok) + } + + _, ok := effects.Join().Invocation() + require.True(t, ok) + }) +} diff --git a/server/handler.go b/server/handler.go index 9b85a56..b3b3d19 100644 --- a/server/handler.go +++ b/server/handler.go @@ -3,7 +3,7 @@ package server import ( "github.com/storacha/go-ucanto/core/invocation" "github.com/storacha/go-ucanto/core/ipld" - "github.com/storacha/go-ucanto/core/receipt" + "github.com/storacha/go-ucanto/core/receipt/fx" "github.com/storacha/go-ucanto/core/result" "github.com/storacha/go-ucanto/core/result/failure" "github.com/storacha/go-ucanto/server/transaction" @@ -11,7 +11,7 @@ import ( "github.com/storacha/go-ucanto/validator" ) -type HandlerFunc[C any, O ipld.Builder] func(capability ucan.Capability[C], invocation invocation.Invocation, context InvocationContext) (out O, fx receipt.Effects, err error) +type HandlerFunc[C any, O ipld.Builder] func(capability ucan.Capability[C], invocation invocation.Invocation, context InvocationContext) (out O, fx fx.Effects, err error) // Provide is used to define given capability provider. It decorates the passed // handler and takes care of UCAN validation. It only calls the handler diff --git a/server/server.go b/server/server.go index 44ff969..5f77a51 100644 --- a/server/server.go +++ b/server/server.go @@ -287,7 +287,7 @@ func Run(server Server, invocation ServiceInvocation) (receipt.AnyReceipt, error fx := tx.Fx() var opts []receipt.Option if fx != nil { - opts = append(opts, receipt.WithJoin(fx.Join()), receipt.WithForks(fx.Fork())) + opts = append(opts, receipt.WithJoin(fx.Join()), receipt.WithFork(fx.Fork()...)) } rcpt, err := receipt.Issue(server.ID(), tx.Out(), ran.FromInvocation(invocation), opts...) diff --git a/server/server_test.go b/server/server_test.go index f55f8cb..2e75120 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -17,6 +17,7 @@ import ( "github.com/storacha/go-ucanto/core/invocation" "github.com/storacha/go-ucanto/core/ipld" "github.com/storacha/go-ucanto/core/receipt" + "github.com/storacha/go-ucanto/core/receipt/fx" "github.com/storacha/go-ucanto/core/result" fdm "github.com/storacha/go-ucanto/core/result/failure/datamodel" "github.com/storacha/go-ucanto/core/schema" @@ -127,7 +128,7 @@ func TestExecute(t *testing.T) { fixtures.Service, WithServiceMethod( uploadadd.Can(), - Provide(uploadadd, func(cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ctx InvocationContext) (uploadAddSuccess, receipt.Effects, error) { + Provide(uploadadd, func(cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ctx InvocationContext) (uploadAddSuccess, fx.Effects, error) { return uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}, nil, nil }), ), @@ -173,7 +174,7 @@ func TestExecute(t *testing.T) { fixtures.Service, WithServiceMethod( uploadadd.Can(), - Provide(uploadadd, func(cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ctx InvocationContext) (uploadAddSuccess, receipt.Effects, error) { + Provide(uploadadd, func(cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ctx InvocationContext) (uploadAddSuccess, fx.Effects, error) { return uploadAddSuccess{Root: cap.Nb().Root, Status: "done"}, nil, nil }), ), @@ -257,7 +258,7 @@ func TestExecute(t *testing.T) { fixtures.Service, WithServiceMethod( uploadadd.Can(), - Provide(uploadadd, func(cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ctx InvocationContext) (uploadAddSuccess, receipt.Effects, error) { + Provide(uploadadd, func(cap ucan.Capability[uploadAddCaveats], inv invocation.Invocation, ctx InvocationContext) (uploadAddSuccess, fx.Effects, error) { return uploadAddSuccess{}, nil, fmt.Errorf("test error") }), ), diff --git a/server/transaction/transaction.go b/server/transaction/transaction.go index d4ad2f5..2770a25 100644 --- a/server/transaction/transaction.go +++ b/server/transaction/transaction.go @@ -1,8 +1,7 @@ package transaction import ( - "github.com/storacha/go-ucanto/core/ipld" - "github.com/storacha/go-ucanto/core/receipt" + "github.com/storacha/go-ucanto/core/receipt/fx" "github.com/storacha/go-ucanto/core/result" ) @@ -10,48 +9,31 @@ import ( // return results that have effects. type Transaction[O any, X any] interface { Out() result.Result[O, X] - Fx() receipt.Effects + Fx() fx.Effects } type transaction[O, X any] struct { out result.Result[O, X] - fx receipt.Effects + fx fx.Effects } func (t transaction[O, X]) Out() result.Result[O, X] { return t.out } -func (t transaction[O, X]) Fx() receipt.Effects { +func (t transaction[O, X]) Fx() fx.Effects { return t.fx } -type effects struct { - fork []ipld.Link - join ipld.Link -} - -func (fx effects) Fork() []ipld.Link { - return fx.fork -} - -func (fx effects) Join() ipld.Link { - return fx.join -} - -func NewEffects(fork []ipld.Link, join ipld.Link) receipt.Effects { - return effects{fork, join} -} - // Option is an option configuring a transaction. type Option func(cfg *txConfig) type txConfig struct { - fx receipt.Effects + fx fx.Effects } // WithEffects configures the effects for the receipt. -func WithEffects(fx receipt.Effects) Option { +func WithEffects(fx fx.Effects) Option { return func(cfg *txConfig) { cfg.fx = fx }