github.com/ethersphere/bee/v2@v2.2.0/pkg/storer/internal/cache/cache_test.go (about)

     1  // Copyright 2022 The Swarm Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package cache_test
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"math"
    12  	"os"
    13  	"sync"
    14  	"testing"
    15  	"time"
    16  
    17  	storage "github.com/ethersphere/bee/v2/pkg/storage"
    18  	"github.com/ethersphere/bee/v2/pkg/storage/inmemchunkstore"
    19  	"github.com/ethersphere/bee/v2/pkg/storage/inmemstore"
    20  	"github.com/ethersphere/bee/v2/pkg/storage/storagetest"
    21  	chunktest "github.com/ethersphere/bee/v2/pkg/storage/testing"
    22  	"github.com/ethersphere/bee/v2/pkg/storer/internal/cache"
    23  	"github.com/ethersphere/bee/v2/pkg/storer/internal/transaction"
    24  	"github.com/ethersphere/bee/v2/pkg/swarm"
    25  	"github.com/google/go-cmp/cmp"
    26  )
    27  
    28  func TestCacheEntryItem(t *testing.T) {
    29  	t.Parallel()
    30  
    31  	tests := []struct {
    32  		name string
    33  		test *storagetest.ItemMarshalAndUnmarshalTest
    34  	}{{
    35  		name: "zero address",
    36  		test: &storagetest.ItemMarshalAndUnmarshalTest{
    37  			Item:       &cache.CacheEntry{},
    38  			Factory:    func() storage.Item { return new(cache.CacheEntry) },
    39  			MarshalErr: cache.ErrMarshalCacheEntryInvalidAddress,
    40  		},
    41  	}, {
    42  		name: "zero values",
    43  		test: &storagetest.ItemMarshalAndUnmarshalTest{
    44  			Item: &cache.CacheEntry{
    45  				Address: swarm.NewAddress(storagetest.MaxAddressBytes[:]),
    46  			},
    47  			Factory:    func() storage.Item { return new(cache.CacheEntry) },
    48  			MarshalErr: cache.ErrMarshalCacheEntryInvalidTimestamp,
    49  		},
    50  	}, {
    51  		name: "max values",
    52  		test: &storagetest.ItemMarshalAndUnmarshalTest{
    53  			Item: &cache.CacheEntry{
    54  				Address:         swarm.NewAddress(storagetest.MaxAddressBytes[:]),
    55  				AccessTimestamp: math.MaxInt64,
    56  			},
    57  			Factory: func() storage.Item { return new(cache.CacheEntry) },
    58  		},
    59  	}, {
    60  		name: "invalid size",
    61  		test: &storagetest.ItemMarshalAndUnmarshalTest{
    62  			Item: &storagetest.ItemStub{
    63  				MarshalBuf:   []byte{0xFF},
    64  				UnmarshalBuf: []byte{0xFF},
    65  			},
    66  			Factory:      func() storage.Item { return new(cache.CacheEntry) },
    67  			UnmarshalErr: cache.ErrUnmarshalCacheEntryInvalidSize,
    68  		},
    69  	}}
    70  
    71  	for _, tc := range tests {
    72  		tc := tc
    73  
    74  		t.Run(fmt.Sprintf("%s marshal/unmarshal", tc.name), func(t *testing.T) {
    75  			t.Parallel()
    76  
    77  			storagetest.TestItemMarshalAndUnmarshal(t, tc.test)
    78  		})
    79  
    80  		t.Run(fmt.Sprintf("%s clone", tc.name), func(t *testing.T) {
    81  			t.Parallel()
    82  
    83  			storagetest.TestItemClone(t, &storagetest.ItemCloneTest{
    84  				Item:    tc.test.Item,
    85  				CmpOpts: tc.test.CmpOpts,
    86  			})
    87  		})
    88  	}
    89  }
    90  
    91  type timeProvider struct {
    92  	t   int64
    93  	mtx sync.Mutex
    94  }
    95  
    96  func (t *timeProvider) Now() func() time.Time {
    97  	return func() time.Time {
    98  		t.mtx.Lock()
    99  		defer t.mtx.Unlock()
   100  		t.t++
   101  		return time.Unix(0, t.t)
   102  	}
   103  }
   104  
   105  func TestMain(m *testing.M) {
   106  	p := &timeProvider{t: time.Now().UnixNano()}
   107  	done := cache.ReplaceTimeNow(p.Now())
   108  	defer func() {
   109  		done()
   110  	}()
   111  	code := m.Run()
   112  	os.Exit(code)
   113  }
   114  
   115  func TestCache(t *testing.T) {
   116  	t.Parallel()
   117  
   118  	t.Run("fresh new cache", func(t *testing.T) {
   119  		t.Parallel()
   120  
   121  		st := newTestStorage(t)
   122  		c, err := cache.New(context.TODO(), st.IndexStore(), 10)
   123  		if err != nil {
   124  			t.Fatal(err)
   125  		}
   126  		verifyCacheState(t, st.IndexStore(), c, swarm.ZeroAddress, swarm.ZeroAddress, 0)
   127  	})
   128  
   129  	t.Run("putter", func(t *testing.T) {
   130  		t.Parallel()
   131  
   132  		st := newTestStorage(t)
   133  		c, err := cache.New(context.TODO(), st.IndexStore(), 10)
   134  		if err != nil {
   135  			t.Fatal(err)
   136  		}
   137  
   138  		chunks := chunktest.GenerateTestRandomChunks(10)
   139  
   140  		t.Run("add till full", func(t *testing.T) {
   141  			for idx, ch := range chunks {
   142  				err := c.Putter(st).Put(context.TODO(), ch)
   143  				if err != nil {
   144  					t.Fatal(err)
   145  				}
   146  				verifyCacheState(t, st.IndexStore(), c, chunks[0].Address(), chunks[idx].Address(), uint64(idx+1))
   147  				verifyCacheOrder(t, c, st.IndexStore(), chunks[:idx+1]...)
   148  			}
   149  		})
   150  
   151  		t.Run("new cache retains state", func(t *testing.T) {
   152  			c2, err := cache.New(context.TODO(), st.IndexStore(), 10)
   153  			if err != nil {
   154  				t.Fatal(err)
   155  			}
   156  			verifyCacheState(t, st.IndexStore(), c2, chunks[0].Address(), chunks[len(chunks)-1].Address(), uint64(len(chunks)))
   157  			verifyCacheOrder(t, c2, st.IndexStore(), chunks...)
   158  		})
   159  	})
   160  
   161  	t.Run("getter", func(t *testing.T) {
   162  		t.Parallel()
   163  
   164  		st := newTestStorage(t)
   165  		c, err := cache.New(context.TODO(), st.IndexStore(), 10)
   166  		if err != nil {
   167  			t.Fatal(err)
   168  		}
   169  
   170  		chunks := chunktest.GenerateTestRandomChunks(10)
   171  
   172  		// this should have no effect on ordering
   173  		t.Run("add and get last", func(t *testing.T) {
   174  			for idx, ch := range chunks {
   175  				err := c.Putter(st).Put(context.TODO(), ch)
   176  				if err != nil {
   177  					t.Fatal(err)
   178  				}
   179  
   180  				readChunk, err := c.Getter(st).Get(context.TODO(), ch.Address())
   181  				if err != nil {
   182  					t.Fatal(err)
   183  				}
   184  				if !readChunk.Equal(ch) {
   185  					t.Fatalf("incorrect chunk: %s", ch.Address())
   186  				}
   187  				verifyCacheState(t, st.IndexStore(), c, chunks[0].Address(), chunks[idx].Address(), uint64(idx+1))
   188  				verifyCacheOrder(t, c, st.IndexStore(), chunks[:idx+1]...)
   189  			}
   190  		})
   191  
   192  		// getting the chunks in reverse order should reverse the ordering in the cache
   193  		// at the end
   194  		t.Run("get reverse order", func(t *testing.T) {
   195  			var newOrder []swarm.Chunk
   196  			for idx := len(chunks) - 1; idx >= 0; idx-- {
   197  				readChunk, err := c.Getter(st).Get(context.TODO(), chunks[idx].Address())
   198  				if err != nil {
   199  					t.Fatal(err)
   200  				}
   201  				if !readChunk.Equal(chunks[idx]) {
   202  					t.Fatalf("incorrect chunk: %s", chunks[idx].Address())
   203  				}
   204  				if idx == 0 {
   205  					// once we access the first entry, the top will change
   206  					verifyCacheState(t, st.IndexStore(), c, chunks[9].Address(), chunks[idx].Address(), 10)
   207  				} else {
   208  					verifyCacheState(t, st.IndexStore(), c, chunks[0].Address(), chunks[idx].Address(), 10)
   209  				}
   210  				newOrder = append(newOrder, chunks[idx])
   211  			}
   212  			verifyCacheOrder(t, c, st.IndexStore(), newOrder...)
   213  		})
   214  
   215  		t.Run("not in chunkstore returns error", func(t *testing.T) {
   216  			for i := 0; i < 5; i++ {
   217  				unknownChunk := chunktest.GenerateTestRandomChunk()
   218  				_, err := c.Getter(st).Get(context.TODO(), unknownChunk.Address())
   219  				if !errors.Is(err, storage.ErrNotFound) {
   220  					t.Fatalf("expected error not found for chunk %s", unknownChunk.Address())
   221  				}
   222  			}
   223  		})
   224  
   225  		t.Run("not in cache doesn't affect state", func(t *testing.T) {
   226  			state := c.State(st.IndexStore())
   227  
   228  			for i := 0; i < 5; i++ {
   229  				extraChunk := chunktest.GenerateTestRandomChunk()
   230  				err := st.Run(context.Background(), func(s transaction.Store) error {
   231  					return s.ChunkStore().Put(context.TODO(), extraChunk)
   232  				})
   233  				if err != nil {
   234  					t.Fatal(err)
   235  				}
   236  
   237  				readChunk, err := c.Getter(st).Get(context.TODO(), extraChunk.Address())
   238  				if err != nil {
   239  					t.Fatal(err)
   240  				}
   241  				if !readChunk.Equal(extraChunk) {
   242  					t.Fatalf("incorrect chunk: %s", extraChunk.Address())
   243  				}
   244  				verifyCacheState(t, st.IndexStore(), c, state.Head, state.Tail, state.Size)
   245  			}
   246  		})
   247  
   248  		t.Run("handle error", func(t *testing.T) {
   249  			t.Parallel()
   250  
   251  			st := newTestStorage(t)
   252  			c, err := cache.New(context.TODO(), st.IndexStore(), 10)
   253  			if err != nil {
   254  				t.Fatal(err)
   255  			}
   256  
   257  			chunks := chunktest.GenerateTestRandomChunks(5)
   258  
   259  			for _, ch := range chunks {
   260  				err := c.Putter(st).Put(context.TODO(), ch)
   261  				if err != nil {
   262  					t.Fatal(err)
   263  				}
   264  			}
   265  			// return error for state update, which occurs at the end of Get/Put operations
   266  			retErr := errors.New("dummy error")
   267  
   268  			st.indexStore.putFn = func(i storage.Item) error {
   269  				if i.Namespace() == "cacheOrderIndex" {
   270  					return retErr
   271  				}
   272  
   273  				return st.indexStore.IndexStore.Put(i)
   274  			}
   275  
   276  			// on error the cache expects the overarching transactions to clean itself up
   277  			// and undo any store updates. So here we only want to ensure the state is
   278  			// reverted to correct one.
   279  			t.Run("put error handling", func(t *testing.T) {
   280  				newChunk := chunktest.GenerateTestRandomChunk()
   281  				err := c.Putter(st).Put(context.TODO(), newChunk)
   282  				if !errors.Is(err, retErr) {
   283  					t.Fatalf("expected error %v during put, found %v", retErr, err)
   284  				}
   285  
   286  				// state should be preserved on failure
   287  				verifyCacheState(t, st.IndexStore(), c, chunks[0].Address(), chunks[4].Address(), 5)
   288  			})
   289  
   290  			t.Run("get error handling", func(t *testing.T) {
   291  				_, err := c.Getter(st).Get(context.TODO(), chunks[2].Address())
   292  				if !errors.Is(err, retErr) {
   293  					t.Fatalf("expected error %v during get, found %v", retErr, err)
   294  				}
   295  
   296  				// state should be preserved on failure
   297  				verifyCacheState(t, st.IndexStore(), c, chunks[0].Address(), chunks[4].Address(), 5)
   298  			})
   299  		})
   300  	})
   301  }
   302  
   303  func TestRemoveOldest(t *testing.T) {
   304  	t.Parallel()
   305  
   306  	st := newTestStorage(t)
   307  	c, err := cache.New(context.Background(), st.IndexStore(), 10)
   308  	if err != nil {
   309  		t.Fatal(err)
   310  	}
   311  
   312  	chunks := chunktest.GenerateTestRandomChunks(30)
   313  
   314  	for _, ch := range chunks {
   315  		err = c.Putter(st).Put(context.Background(), ch)
   316  		if err != nil {
   317  			t.Fatal(err)
   318  		}
   319  	}
   320  
   321  	verifyCacheState(t, st.IndexStore(), c, chunks[0].Address(), chunks[29].Address(), 30)
   322  	verifyCacheOrder(t, c, st.IndexStore(), chunks...)
   323  
   324  	err = c.RemoveOldestMaxBatch(context.Background(), st, 30, 5)
   325  	if err != nil {
   326  		t.Fatal(err)
   327  	}
   328  
   329  	verifyCacheState(t, st.IndexStore(), c, swarm.ZeroAddress, swarm.ZeroAddress, 0)
   330  
   331  	verifyChunksDeleted(t, st.ChunkStore(), chunks...)
   332  }
   333  
   334  func TestShallowCopy(t *testing.T) {
   335  	t.Parallel()
   336  
   337  	st := newTestStorage(t)
   338  	c, err := cache.New(context.Background(), st.IndexStore(), 10)
   339  	if err != nil {
   340  		t.Fatal(err)
   341  	}
   342  
   343  	chunks := chunktest.GenerateTestRandomChunks(10)
   344  	chunksToMove := make([]swarm.Address, 0, 10)
   345  
   346  	// add the chunks to chunkstore. This simulates the reserve already populating
   347  	// the chunkstore with chunks.
   348  	for _, ch := range chunks {
   349  
   350  		err := st.Run(context.Background(), func(s transaction.Store) error {
   351  			return s.ChunkStore().Put(context.Background(), ch)
   352  		})
   353  		if err != nil {
   354  			t.Fatal(err)
   355  		}
   356  		chunksToMove = append(chunksToMove, ch.Address())
   357  	}
   358  
   359  	err = c.ShallowCopy(context.Background(), st, chunksToMove...)
   360  	if err != nil {
   361  		t.Fatal(err)
   362  	}
   363  
   364  	verifyCacheState(t, st.IndexStore(), c, chunks[0].Address(), chunks[9].Address(), 10)
   365  	verifyCacheOrder(t, c, st.IndexStore(), chunks...)
   366  
   367  	// move again, should be no-op
   368  	err = c.ShallowCopy(context.Background(), st, chunksToMove...)
   369  	if err != nil {
   370  		t.Fatal(err)
   371  	}
   372  
   373  	verifyCacheState(t, st.IndexStore(), c, chunks[0].Address(), chunks[9].Address(), 10)
   374  	verifyCacheOrder(t, c, st.IndexStore(), chunks...)
   375  
   376  	chunks1 := chunktest.GenerateTestRandomChunks(10)
   377  	chunksToMove1 := make([]swarm.Address, 0, 10)
   378  
   379  	// add the chunks to chunkstore. This simulates the reserve already populating
   380  	// the chunkstore with chunks.
   381  	for _, ch := range chunks1 {
   382  		err := st.Run(context.Background(), func(s transaction.Store) error {
   383  			return s.ChunkStore().Put(context.Background(), ch)
   384  		})
   385  		if err != nil {
   386  			t.Fatal(err)
   387  		}
   388  		chunksToMove1 = append(chunksToMove1, ch.Address())
   389  	}
   390  
   391  	// move new chunks
   392  	err = c.ShallowCopy(context.Background(), st, chunksToMove1...)
   393  	if err != nil {
   394  		t.Fatal(err)
   395  	}
   396  
   397  	verifyCacheState(t, st.IndexStore(), c, chunks[0].Address(), chunks1[9].Address(), 20)
   398  	verifyCacheOrder(t, c, st.IndexStore(), append(chunks, chunks1...)...)
   399  
   400  	err = c.RemoveOldest(context.Background(), st, 10)
   401  	if err != nil {
   402  		t.Fatal(err)
   403  	}
   404  
   405  	verifyChunksDeleted(t, st.ChunkStore(), chunks...)
   406  }
   407  
   408  func TestShallowCopyOverCap(t *testing.T) {
   409  	t.Parallel()
   410  
   411  	st := newTestStorage(t)
   412  	c, err := cache.New(context.Background(), st.IndexStore(), 10)
   413  	if err != nil {
   414  		t.Fatal(err)
   415  	}
   416  
   417  	chunks := chunktest.GenerateTestRandomChunks(15)
   418  	chunksToMove := make([]swarm.Address, 0, 15)
   419  
   420  	// add the chunks to chunkstore. This simulates the reserve already populating
   421  	// the chunkstore with chunks.
   422  	for _, ch := range chunks {
   423  
   424  		err := st.Run(context.Background(), func(s transaction.Store) error {
   425  			return s.ChunkStore().Put(context.Background(), ch)
   426  		})
   427  		if err != nil {
   428  			t.Fatal(err)
   429  		}
   430  		chunksToMove = append(chunksToMove, ch.Address())
   431  	}
   432  
   433  	// move new chunks
   434  	err = c.ShallowCopy(context.Background(), st, chunksToMove...)
   435  	if err != nil {
   436  		t.Fatal(err)
   437  	}
   438  
   439  	verifyCacheState(t, st.IndexStore(), c, chunks[5].Address(), chunks[14].Address(), 10)
   440  	verifyCacheOrder(t, c, st.IndexStore(), chunks[5:15]...)
   441  
   442  	err = c.RemoveOldest(context.Background(), st, 5)
   443  	if err != nil {
   444  		t.Fatal(err)
   445  	}
   446  
   447  	verifyChunksDeleted(t, st.ChunkStore(), chunks[5:10]...)
   448  }
   449  
   450  func TestShallowCopyAlreadyCached(t *testing.T) {
   451  	t.Parallel()
   452  
   453  	st := newTestStorage(t)
   454  	c, err := cache.New(context.Background(), st.IndexStore(), 1000)
   455  	if err != nil {
   456  		t.Fatal(err)
   457  	}
   458  
   459  	chunks := chunktest.GenerateTestRandomChunks(10)
   460  	chunksToMove := make([]swarm.Address, 0, 10)
   461  
   462  	for _, ch := range chunks {
   463  		// add the chunks to chunkstore. This simulates the reserve already populating the chunkstore with chunks.
   464  
   465  		err := st.Run(context.Background(), func(s transaction.Store) error {
   466  			return s.ChunkStore().Put(context.Background(), ch)
   467  		})
   468  		if err != nil {
   469  			t.Fatal(err)
   470  		}
   471  		// already cached
   472  		err = c.Putter(st).Put(context.Background(), ch)
   473  		if err != nil {
   474  			t.Fatal(err)
   475  		}
   476  		chunksToMove = append(chunksToMove, ch.Address())
   477  	}
   478  
   479  	// move new chunks
   480  	err = c.ShallowCopy(context.Background(), st, chunksToMove...)
   481  	if err != nil {
   482  		t.Fatal(err)
   483  	}
   484  
   485  	verifyChunksExist(t, st.ChunkStore(), chunks...)
   486  
   487  	err = c.RemoveOldest(context.Background(), st, 10)
   488  	if err != nil {
   489  		t.Fatal(err)
   490  	}
   491  
   492  	verifyChunksDeleted(t, st.ChunkStore(), chunks...)
   493  }
   494  
   495  func verifyCacheState(
   496  	t *testing.T,
   497  	store storage.Reader,
   498  	c *cache.Cache,
   499  	expStart, expEnd swarm.Address,
   500  	expCount uint64,
   501  ) {
   502  	t.Helper()
   503  
   504  	state := c.State(store)
   505  	expState := cache.CacheState{Head: expStart, Tail: expEnd, Size: expCount}
   506  
   507  	if diff := cmp.Diff(expState, state); diff != "" {
   508  		t.Fatalf("state mismatch (-want +have):\n%s", diff)
   509  	}
   510  }
   511  
   512  func verifyCacheOrder(
   513  	t *testing.T,
   514  	c *cache.Cache,
   515  	st storage.Reader,
   516  	chs ...swarm.Chunk,
   517  ) {
   518  	t.Helper()
   519  
   520  	state := c.State(st)
   521  
   522  	if uint64(len(chs)) != state.Size {
   523  		t.Fatalf("unexpected count, exp: %d found: %d", state.Size, len(chs))
   524  	}
   525  
   526  	idx := 0
   527  	err := c.IterateOldToNew(st, state.Head, state.Tail, func(entry swarm.Address) (bool, error) {
   528  		if !chs[idx].Address().Equal(entry) {
   529  			return true, fmt.Errorf(
   530  				"incorrect order of cache items, idx: %d exp: %s found: %s",
   531  				idx, chs[idx].Address(), entry,
   532  			)
   533  		}
   534  		idx++
   535  		return false, nil
   536  	})
   537  	if err != nil {
   538  		t.Fatalf("failed at index %d err %s", idx, err)
   539  	}
   540  }
   541  
   542  func verifyChunksDeleted(
   543  	t *testing.T,
   544  	chStore storage.ReadOnlyChunkStore,
   545  	chs ...swarm.Chunk,
   546  ) {
   547  	t.Helper()
   548  
   549  	for _, ch := range chs {
   550  		found, err := chStore.Has(context.TODO(), ch.Address())
   551  		if err != nil {
   552  			t.Fatal(err)
   553  		}
   554  		if found {
   555  			t.Fatalf("chunk %s expected to not be found but exists", ch.Address())
   556  		}
   557  		_, err = chStore.Get(context.TODO(), ch.Address())
   558  		if !errors.Is(err, storage.ErrNotFound) {
   559  			t.Fatalf("expected error %v but found %v", storage.ErrNotFound, err)
   560  		}
   561  	}
   562  }
   563  
   564  func verifyChunksExist(
   565  	t *testing.T,
   566  	chStore storage.ReadOnlyChunkStore,
   567  	chs ...swarm.Chunk,
   568  ) {
   569  	t.Helper()
   570  
   571  	for _, ch := range chs {
   572  		found, err := chStore.Has(context.TODO(), ch.Address())
   573  		if err != nil {
   574  			t.Fatal(err)
   575  		}
   576  		if !found {
   577  			t.Fatalf("chunk %s expected to be found but not exists", ch.Address())
   578  		}
   579  	}
   580  }
   581  
   582  type inmemStorage struct {
   583  	indexStore *customIndexStore
   584  	chunkStore storage.ChunkStore
   585  }
   586  
   587  func newTestStorage(t *testing.T) *inmemStorage {
   588  	t.Helper()
   589  
   590  	ts := &inmemStorage{
   591  		indexStore: &customIndexStore{inmemstore.New(), nil},
   592  		chunkStore: inmemchunkstore.New(),
   593  	}
   594  
   595  	return ts
   596  }
   597  
   598  type customIndexStore struct {
   599  	storage.IndexStore
   600  	putFn func(storage.Item) error
   601  }
   602  
   603  func (s *customIndexStore) Put(i storage.Item) error {
   604  	if s.putFn != nil {
   605  		return s.putFn(i)
   606  	}
   607  	return s.IndexStore.Put(i)
   608  }
   609  
   610  func (t *inmemStorage) NewTransaction(ctx context.Context) (transaction.Transaction, func()) {
   611  	return &inmemTrx{t.indexStore, t.chunkStore}, func() {}
   612  }
   613  
   614  type inmemTrx struct {
   615  	indexStore storage.IndexStore
   616  	chunkStore storage.ChunkStore
   617  }
   618  
   619  func (t *inmemStorage) IndexStore() storage.Reader             { return t.indexStore }
   620  func (t *inmemStorage) ChunkStore() storage.ReadOnlyChunkStore { return t.chunkStore }
   621  
   622  func (t *inmemTrx) IndexStore() storage.IndexStore { return t.indexStore }
   623  func (t *inmemTrx) ChunkStore() storage.ChunkStore { return t.chunkStore }
   624  func (t *inmemTrx) Commit() error                  { return nil }
   625  
   626  func (t *inmemStorage) Close() error { return nil }
   627  func (t *inmemStorage) Run(ctx context.Context, f func(s transaction.Store) error) error {
   628  	trx, done := t.NewTransaction(ctx)
   629  	defer done()
   630  	return f(trx)
   631  }