Skip to content

Commit

Permalink
feat: support invocations as effects (#30)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
alanshaw authored Oct 28, 2024
1 parent 42d9e1d commit a72237a
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 57 deletions.
78 changes: 78 additions & 0 deletions core/receipt/fx/fx.go
Original file line number Diff line number Diff line change
@@ -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}
}
76 changes: 49 additions & 27 deletions core/receipt/receipt.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,21 @@ 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.
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
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -228,16 +234,16 @@ 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
}
}

// 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
Expand Down Expand Up @@ -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{}
Expand Down
85 changes: 85 additions & 0 deletions core/receipt/receipt_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
4 changes: 2 additions & 2 deletions server/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ 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"
"github.com/storacha/go-ucanto/ucan"
"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
Expand Down
2 changes: 1 addition & 1 deletion server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
Expand Down
7 changes: 4 additions & 3 deletions server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}),
),
Expand Down Expand Up @@ -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
}),
),
Expand Down Expand Up @@ -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")
}),
),
Expand Down
Loading

0 comments on commit a72237a

Please sign in to comment.