github.com/storacha/go-ucanto@v0.7.2/core/receipt/receipt_test.go (about)

     1  package receipt
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"slices"
     7  	"testing"
     8  
     9  	ipldprime "github.com/ipld/go-ipld-prime"
    10  	"github.com/ipld/go-ipld-prime/schema"
    11  	"github.com/storacha/go-ucanto/core/dag/blockstore"
    12  	"github.com/storacha/go-ucanto/core/delegation"
    13  	"github.com/storacha/go-ucanto/core/invocation"
    14  	"github.com/storacha/go-ucanto/core/ipld"
    15  	"github.com/storacha/go-ucanto/core/ipld/block"
    16  	"github.com/storacha/go-ucanto/core/receipt/fx"
    17  	"github.com/storacha/go-ucanto/core/receipt/ran"
    18  	"github.com/storacha/go-ucanto/core/result"
    19  	"github.com/storacha/go-ucanto/core/result/ok"
    20  	"github.com/storacha/go-ucanto/testing/fixtures"
    21  	"github.com/storacha/go-ucanto/testing/helpers"
    22  	"github.com/storacha/go-ucanto/ucan"
    23  	"github.com/stretchr/testify/assert"
    24  	"github.com/stretchr/testify/require"
    25  )
    26  
    27  func TestEffects(t *testing.T) {
    28  	ran := ran.FromLink(helpers.RandomCID())
    29  	out := result.Ok[ok.Unit, ipld.Builder](ok.Unit{})
    30  
    31  	t.Run("as links", func(t *testing.T) {
    32  		f0 := fx.FromLink(helpers.RandomCID())
    33  		f1 := fx.FromLink(helpers.RandomCID())
    34  		j := fx.FromLink(helpers.RandomCID())
    35  
    36  		receipt, err := Issue(fixtures.Alice, out, ran, WithFork(f0, f1), WithJoin(j))
    37  		require.NoError(t, err)
    38  
    39  		effects := receipt.Fx()
    40  		require.True(t, slices.ContainsFunc(effects.Fork(), func(f fx.Effect) bool {
    41  			return f.Link().String() == f0.Link().String()
    42  		}))
    43  		require.True(t, slices.ContainsFunc(effects.Fork(), func(f fx.Effect) bool {
    44  			return f.Link().String() == f1.Link().String()
    45  		}))
    46  		require.Equal(t, effects.Join().Link(), j.Link())
    47  	})
    48  
    49  	t.Run("as invocations", func(t *testing.T) {
    50  		i0, err := invocation.Invoke(
    51  			fixtures.Alice,
    52  			fixtures.Bob,
    53  			ucan.NewCapability("fx/0", fixtures.Alice.DID().String(), ucan.NoCaveats{}),
    54  		)
    55  		require.NoError(t, err)
    56  		i1, err := invocation.Invoke(
    57  			fixtures.Alice,
    58  			fixtures.Mallory,
    59  			ucan.NewCapability("fx/1", fixtures.Alice.DID().String(), ucan.NoCaveats{}),
    60  		)
    61  		require.NoError(t, err)
    62  		i2, err := invocation.Invoke(
    63  			fixtures.Mallory,
    64  			fixtures.Bob,
    65  			ucan.NewCapability("fx/2", fixtures.Alice.DID().String(), ucan.NoCaveats{}),
    66  		)
    67  		require.NoError(t, err)
    68  
    69  		f0 := fx.FromInvocation(i0)
    70  		f1 := fx.FromInvocation(i1)
    71  		j := fx.FromInvocation(i2)
    72  
    73  		receipt, err := Issue(fixtures.Alice, out, ran, WithFork(f0, f1), WithJoin(j))
    74  		require.NoError(t, err)
    75  
    76  		effects := receipt.Fx()
    77  		require.True(t, slices.ContainsFunc(effects.Fork(), func(f fx.Effect) bool {
    78  			return f.Link().String() == f0.Link().String()
    79  		}))
    80  		require.True(t, slices.ContainsFunc(effects.Fork(), func(f fx.Effect) bool {
    81  			return f.Link().String() == f1.Link().String()
    82  		}))
    83  		require.Equal(t, effects.Join().Link(), j.Link())
    84  
    85  		for _, effect := range effects.Fork() {
    86  			_, ok := effect.Invocation()
    87  			require.True(t, ok)
    88  		}
    89  
    90  		_, ok := effects.Join().Invocation()
    91  		require.True(t, ok)
    92  	})
    93  }
    94  
    95  var someTS = mustLoadTS()
    96  
    97  func mustLoadTS() *schema.TypeSystem {
    98  	someSchema := []byte(`
    99  		type someOkType struct {
   100  			someOkProperty String
   101  		}
   102  
   103  		type someErrorType struct {
   104  			someErrorProperty String
   105  		}
   106  	`)
   107  	ts, err := ipldprime.LoadSchemaBytes(someSchema)
   108  	if err != nil {
   109  		panic(fmt.Errorf("loading some schema: %w", err))
   110  	}
   111  
   112  	return ts
   113  }
   114  
   115  type someOkType struct {
   116  	SomeOkProperty string
   117  }
   118  
   119  func (s someOkType) ToIPLD() (ipld.Node, error) {
   120  	return ipld.WrapWithRecovery(&s, someTS.TypeByName("someOkType"))
   121  }
   122  
   123  type someErrorType struct {
   124  	SomeErrorProperty string
   125  }
   126  
   127  func (s someErrorType) ToIPLD() (ipld.Node, error) {
   128  	return ipld.WrapWithRecovery(&s, someTS.TypeByName("someErrorType"))
   129  }
   130  
   131  func TestIssue(t *testing.T) {
   132  	t.Run("ran as invocation", func(t *testing.T) {
   133  		inv, err := invocation.Invoke(
   134  			fixtures.Alice,
   135  			fixtures.Bob,
   136  			ucan.NewCapability("ran/invoke", fixtures.Alice.DID().String(), ucan.NoCaveats{}),
   137  		)
   138  		require.NoError(t, err)
   139  		ran := ran.FromInvocation(inv)
   140  
   141  		out := result.Ok[someOkType, someErrorType](someOkType{SomeOkProperty: "some ok value"})
   142  
   143  		issuedRcpt, err := Issue(fixtures.Alice, out, ran)
   144  		require.NoError(t, err)
   145  
   146  		ranInv, ok := issuedRcpt.Ran().Invocation()
   147  		require.True(t, ok)
   148  		require.Equal(t, inv.Link().String(), ranInv.Link().String())
   149  	})
   150  
   151  	t.Run("ran as link", func(t *testing.T) {
   152  		inv, err := invocation.Invoke(
   153  			fixtures.Alice,
   154  			fixtures.Bob,
   155  			ucan.NewCapability("ran/invoke", fixtures.Alice.DID().String(), ucan.NoCaveats{}),
   156  		)
   157  		require.NoError(t, err)
   158  		ran := ran.FromLink(inv.Link())
   159  
   160  		out := result.Ok[someOkType, someErrorType](someOkType{SomeOkProperty: "some ok value"})
   161  
   162  		issuedRcpt, err := Issue(fixtures.Alice, out, ran)
   163  		require.NoError(t, err)
   164  
   165  		ranInv, ok := issuedRcpt.Ran().Invocation()
   166  		require.False(t, ok)
   167  		require.Nil(t, ranInv)
   168  
   169  		ranInvLink := issuedRcpt.Ran().Link()
   170  		require.NotNil(t, ranInvLink)
   171  		require.Equal(t, inv.Link().String(), ranInvLink.String())
   172  	})
   173  }
   174  
   175  func TestVerifySignature(t *testing.T) {
   176  	// Setup test data
   177  	r := ran.FromLink(helpers.RandomCID())
   178  	out := result.Ok[someOkType, someErrorType](someOkType{SomeOkProperty: "foo"})
   179  
   180  	// Create a valid receipt
   181  	rcpt, err := Issue(fixtures.Alice, out, r)
   182  	require.NoError(t, err)
   183  
   184  	t.Run("verifies valid signature", func(t *testing.T) {
   185  		valid, err := rcpt.VerifySignature(fixtures.Alice.Verifier())
   186  		require.NoError(t, err)
   187  		require.True(t, valid)
   188  	})
   189  
   190  	t.Run("fails with wrong verifier", func(t *testing.T) {
   191  		valid, err := rcpt.VerifySignature(fixtures.Bob.Verifier())
   192  		require.NoError(t, err)
   193  		require.False(t, valid)
   194  	})
   195  
   196  	t.Run("fails with tampered receipt", func(t *testing.T) {
   197  		// Create a new receipt with the same data but different signature
   198  		someRcpt, err := Rebind[someOkType, someErrorType](rcpt, someTS.TypeByName("someOkType"), someTS.TypeByName("someErrorType"))
   199  		require.NoError(t, err)
   200  
   201  		tamperedRcpt, ok := someRcpt.(*receipt[someOkType, someErrorType])
   202  		require.True(t, ok)
   203  
   204  		// Tamper with the receipt data
   205  		// SomeOkProperty was "foo" in the original receipt
   206  		assert.Equal(t, tamperedRcpt.data.Ocm.Out.Ok, &someOkType{SomeOkProperty: "foo"})
   207  		tamperedRcpt.data.Ocm.Out.Ok = &someOkType{SomeOkProperty: "bar"}
   208  
   209  		// Should fail with original verifier
   210  		valid, err := tamperedRcpt.VerifySignature(fixtures.Alice.Verifier())
   211  		require.NoError(t, err)
   212  		require.False(t, valid)
   213  	})
   214  }
   215  
   216  func TestArchiveExtract(t *testing.T) {
   217  	prf, err := delegation.Delegate(
   218  		fixtures.Alice,
   219  		fixtures.Bob,
   220  		[]ucan.Capability[ucan.NoCaveats]{
   221  			ucan.NewCapability("test/proof", fixtures.Alice.DID().String(), ucan.NoCaveats{}),
   222  		},
   223  	)
   224  	require.NoError(t, err)
   225  
   226  	inv, err := invocation.Invoke(
   227  		fixtures.Alice,
   228  		fixtures.Bob,
   229  		ucan.NewCapability("test/attach", fixtures.Alice.DID().String(), ucan.NoCaveats{}),
   230  	)
   231  	require.NoError(t, err)
   232  
   233  	ran := ran.FromInvocation(inv)
   234  	ok := someOkType{SomeOkProperty: "some ok value"}
   235  	rcpt, err := Issue(
   236  		fixtures.Alice,
   237  		result.Ok[someOkType, someErrorType](ok),
   238  		ran,
   239  		WithProofs(delegation.Proofs{
   240  			delegation.FromDelegation(prf),
   241  			// include an absent proof to prove things don't break - PUN INTENDED
   242  			delegation.FromLink(helpers.RandomCID()),
   243  		}),
   244  	)
   245  	require.NoError(t, err)
   246  
   247  	archive := rcpt.Archive()
   248  
   249  	archiveBytes, err := io.ReadAll(archive)
   250  	require.NoError(t, err)
   251  	extracted, err := Extract(archiveBytes)
   252  	require.NoError(t, err)
   253  
   254  	var rcptBlks []ipld.Block
   255  	for b, err := range rcpt.Export() {
   256  		require.NoError(t, err)
   257  		rcptBlks = append(rcptBlks, b)
   258  	}
   259  
   260  	var extractedBlks []ipld.Block
   261  	for b, err := range extracted.Export() {
   262  		require.NoError(t, err)
   263  		extractedBlks = append(extractedBlks, b)
   264  	}
   265  
   266  	require.Equal(t, len(rcptBlks), len(extractedBlks))
   267  	for i, b := range rcptBlks {
   268  		require.Equal(t, b.Link().String(), extractedBlks[i].Link().String())
   269  	}
   270  }
   271  
   272  func TestExport(t *testing.T) {
   273  	prf, err := delegation.Delegate(
   274  		fixtures.Alice,
   275  		fixtures.Bob,
   276  		[]ucan.Capability[ucan.NoCaveats]{
   277  			ucan.NewCapability("test/proof", fixtures.Alice.DID().String(), ucan.NoCaveats{}),
   278  		},
   279  	)
   280  	require.NoError(t, err)
   281  
   282  	inv, err := invocation.Invoke(
   283  		fixtures.Alice,
   284  		fixtures.Bob,
   285  		ucan.NewCapability("test/export", fixtures.Alice.DID().String(), ucan.NoCaveats{}),
   286  	)
   287  	require.NoError(t, err)
   288  
   289  	forkFx := fx.FromInvocation(
   290  		helpers.Must(
   291  			invocation.Invoke(
   292  				fixtures.Alice,
   293  				fixtures.Bob,
   294  				ucan.NewCapability("test/fx/fork", fixtures.Alice.DID().String(), ucan.NoCaveats{}),
   295  			),
   296  		),
   297  	)
   298  
   299  	joinFx := fx.FromInvocation(
   300  		helpers.Must(
   301  			invocation.Invoke(
   302  				fixtures.Alice,
   303  				fixtures.Bob,
   304  				ucan.NewCapability("test/fx/join", fixtures.Alice.DID().String(), ucan.NoCaveats{}),
   305  			),
   306  		),
   307  	)
   308  
   309  	ran := ran.FromInvocation(inv)
   310  	ok := someOkType{SomeOkProperty: "some ok value"}
   311  	rcpt, err := Issue(
   312  		fixtures.Alice,
   313  		result.Ok[someOkType, someErrorType](ok),
   314  		ran,
   315  		WithFork(forkFx, fx.FromLink(helpers.RandomCID())),
   316  		WithJoin(joinFx),
   317  		WithProofs(delegation.Proofs{
   318  			delegation.FromDelegation(prf),
   319  			// include an absent proof to prove things don't break - PUN INTENDED
   320  			delegation.FromLink(helpers.RandomCID()),
   321  		}),
   322  	)
   323  	require.NoError(t, err)
   324  
   325  	bs, err := blockstore.NewBlockStore()
   326  	require.NoError(t, err)
   327  
   328  	var blks []ipld.Block
   329  	for b, err := range rcpt.Blocks() {
   330  		require.NoError(t, err)
   331  		require.NoError(t, bs.Put(b))
   332  		blks = append(blks, b)
   333  	}
   334  	require.Len(t, blks, 5)
   335  	require.True(t, slices.ContainsFunc(blks, func(b ipld.Block) bool {
   336  		return b.Link().String() == prf.Link().String()
   337  	}))
   338  	require.True(t, slices.ContainsFunc(blks, func(b ipld.Block) bool {
   339  		return b.Link().String() == inv.Link().String()
   340  	}))
   341  	require.True(t, slices.ContainsFunc(blks, func(b ipld.Block) bool {
   342  		return b.Link().String() == rcpt.Root().Link().String()
   343  	}))
   344  
   345  	// add an additional block to the blockstore that is not linked to by the receipt
   346  	otherblk := block.NewBlock(helpers.RandomCID(), helpers.RandomBytes(32))
   347  	err = bs.Put(otherblk)
   348  	require.NoError(t, err)
   349  
   350  	// reinstantiate receipt with our new blockstore
   351  	rcpt, err = NewAnyReceipt(rcpt.Root().Link(), bs)
   352  	require.NoError(t, err)
   353  
   354  	var exblks []ipld.Block
   355  	// export the receipt from the blockstore
   356  	for b, err := range rcpt.Export() {
   357  		require.NoError(t, err)
   358  		exblks = append(exblks, b)
   359  	}
   360  
   361  	// expect exblks to have the same blocks in the same order and it should not
   362  	// include otherblk
   363  	require.Len(t, exblks, len(blks))
   364  	for i, b := range blks {
   365  		require.Equal(t, b.Link().String(), exblks[i].Link().String())
   366  	}
   367  
   368  	// expect rcpt.Blocks() to include otherblk though...
   369  	var blklnks []string
   370  	for b, err := range rcpt.Blocks() {
   371  		require.NoError(t, err)
   372  		blklnks = append(blklnks, b.Link().String())
   373  	}
   374  	require.Contains(t, blklnks, otherblk.Link().String())
   375  }
   376  
   377  func TestAttachInvocation(t *testing.T) {
   378  	inv, err := invocation.Invoke(
   379  		fixtures.Alice,
   380  		fixtures.Bob,
   381  		ucan.NewCapability("ran/invoke", fixtures.Alice.DID().String(), ucan.NoCaveats{}),
   382  	)
   383  	require.NoError(t, err)
   384  
   385  	out := result.Ok[someOkType, someErrorType](someOkType{SomeOkProperty: "some ok value"})
   386  
   387  	t.Run("adds invocation to receipt without one", func(t *testing.T) {
   388  		issuedRcpt, err := Issue(fixtures.Alice, out, ran.FromLink(inv.Link()))
   389  		require.NoError(t, err)
   390  
   391  		ranInv, ok := issuedRcpt.Ran().Invocation()
   392  		require.False(t, ok)
   393  		require.Nil(t, ranInv)
   394  
   395  		err = issuedRcpt.AttachInvocation(inv)
   396  		require.NoError(t, err)
   397  
   398  		ranInv, ok = issuedRcpt.Ran().Invocation()
   399  		require.True(t, ok)
   400  		require.Equal(t, inv.Link().String(), ranInv.Link().String())
   401  	})
   402  
   403  	t.Run("doesn't fail if receipt already has invocation and invocations match", func(t *testing.T) {
   404  		issuedRcpt, err := Issue(fixtures.Alice, out, ran.FromInvocation(inv))
   405  		require.NoError(t, err)
   406  
   407  		ranInv, ok := issuedRcpt.Ran().Invocation()
   408  		require.True(t, ok)
   409  		require.Equal(t, inv.Link().String(), ranInv.Link().String())
   410  
   411  		err = issuedRcpt.AttachInvocation(inv)
   412  		require.NoError(t, err)
   413  	})
   414  
   415  	t.Run("fails if receipt invocations don't match", func(t *testing.T) {
   416  		issuedRcpt, err := Issue(fixtures.Alice, out, ran.FromLink(inv.Link()))
   417  		require.NoError(t, err)
   418  
   419  		inv2, err := invocation.Invoke(
   420  			fixtures.Alice,
   421  			fixtures.Service, // previous invocation's audience is Bob
   422  			ucan.NewCapability("ran/invoke", fixtures.Alice.DID().String(), ucan.NoCaveats{}),
   423  		)
   424  		require.NoError(t, err)
   425  
   426  		err = issuedRcpt.AttachInvocation(inv2)
   427  		require.Error(t, err)
   428  	})
   429  }
   430  
   431  func TestClone(t *testing.T) {
   432  	inv, err := invocation.Invoke(
   433  		fixtures.Alice,
   434  		fixtures.Bob,
   435  		ucan.NewCapability("ran/invoke", fixtures.Alice.DID().String(), ucan.NoCaveats{}),
   436  	)
   437  	require.NoError(t, err)
   438  
   439  	out := result.Ok[someOkType, someErrorType](someOkType{SomeOkProperty: "some ok value"})
   440  
   441  	rcpt1, err := Issue(fixtures.Alice, out, ran.FromLink(inv.Link()))
   442  	require.NoError(t, err)
   443  
   444  	rcpt2, err := rcpt1.Clone()
   445  	require.NoError(t, err)
   446  
   447  	// attach an invocation to rcpt2 and confirm it doesn't affect rcpt1
   448  	err = rcpt2.AttachInvocation(inv)
   449  	require.NoError(t, err)
   450  
   451  	rcpt1NumBlocks := 0
   452  	for range rcpt1.Blocks() {
   453  		rcpt1NumBlocks++
   454  	}
   455  	rcpt2NumBlocks := 0
   456  	for range rcpt2.Blocks() {
   457  		rcpt2NumBlocks++
   458  	}
   459  	require.True(t, rcpt2NumBlocks > rcpt1NumBlocks)
   460  }
   461  
   462  func TestAnyReceiptReader(t *testing.T) {
   463  	ranInv, err := invocation.Invoke(
   464  		fixtures.Alice,
   465  		fixtures.Bob,
   466  		ucan.NewCapability("ran/invoke", fixtures.Alice.DID().String(), ucan.NoCaveats{}),
   467  	)
   468  	require.NoError(t, err)
   469  	ran := ran.FromInvocation(ranInv)
   470  
   471  	out := result.Ok[someOkType, someErrorType](someOkType{SomeOkProperty: "some ok value"})
   472  
   473  	issuedRcpt, err := Issue(fixtures.Alice, out, ran)
   474  	require.NoError(t, err)
   475  
   476  	reader := NewAnyReceiptReader()
   477  	var anyRcpt AnyReceipt
   478  	anyRcpt, err = reader.Read(issuedRcpt.Root().Link(), issuedRcpt.Blocks())
   479  	require.NoError(t, err)
   480  
   481  	concreteRcpt, err := Rebind[*someOkType, *someErrorType](anyRcpt, someTS.TypeByName("someOkType"), someTS.TypeByName("someErrorType"))
   482  	require.NoError(t, err)
   483  
   484  	someOk, someErr := result.Unwrap(concreteRcpt.Out())
   485  	require.Equal(t, "some ok value", someOk.SomeOkProperty)
   486  	require.Nil(t, someErr)
   487  }