github.com/iotexproject/iotex-core@v1.14.1-rc1/blockchain/blockdao/blockdao_test.go (about)

     1  package blockdao
     2  
     3  import (
     4  	"context"
     5  	"hash/fnv"
     6  	"math/big"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/pkg/errors"
    11  	"github.com/stretchr/testify/require"
    12  
    13  	"github.com/iotexproject/go-pkgs/hash"
    14  
    15  	"github.com/iotexproject/iotex-core/action"
    16  	"github.com/iotexproject/iotex-core/action/protocol"
    17  	"github.com/iotexproject/iotex-core/blockchain/block"
    18  	"github.com/iotexproject/iotex-core/blockchain/filedao"
    19  	"github.com/iotexproject/iotex-core/blockchain/genesis"
    20  	"github.com/iotexproject/iotex-core/db"
    21  	"github.com/iotexproject/iotex-core/pkg/compress"
    22  	"github.com/iotexproject/iotex-core/pkg/unit"
    23  	"github.com/iotexproject/iotex-core/test/identityset"
    24  	"github.com/iotexproject/iotex-core/testutil"
    25  )
    26  
    27  func getTestBlocks(t *testing.T) []*block.Block {
    28  	amount := uint64(50 << 22)
    29  	tsf1, err := action.SignedTransfer(identityset.Address(28).String(), identityset.PrivateKey(28), 1, big.NewInt(int64(amount)), nil, testutil.TestGasLimit, big.NewInt(0))
    30  	require.NoError(t, err)
    31  
    32  	tsf2, err := action.SignedTransfer(identityset.Address(29).String(), identityset.PrivateKey(29), 2, big.NewInt(int64(amount)), nil, testutil.TestGasLimit, big.NewInt(0))
    33  	require.NoError(t, err)
    34  
    35  	tsf3, err := action.SignedTransfer(identityset.Address(30).String(), identityset.PrivateKey(30), 3, big.NewInt(int64(amount)), nil, testutil.TestGasLimit, big.NewInt(0))
    36  	require.NoError(t, err)
    37  
    38  	tsf4, err := action.SignedTransfer(identityset.Address(29).String(), identityset.PrivateKey(28), 2, big.NewInt(int64(amount)), nil, testutil.TestGasLimit, big.NewInt(0))
    39  	require.NoError(t, err)
    40  
    41  	tsf5, err := action.SignedTransfer(identityset.Address(30).String(), identityset.PrivateKey(29), 3, big.NewInt(int64(amount)), nil, testutil.TestGasLimit, big.NewInt(0))
    42  	require.NoError(t, err)
    43  
    44  	tsf6, err := action.SignedTransfer(identityset.Address(28).String(), identityset.PrivateKey(30), 4, big.NewInt(int64(amount)), nil, testutil.TestGasLimit, big.NewInt(0))
    45  	require.NoError(t, err)
    46  
    47  	// create testing executions
    48  	execution1, err := action.SignedExecution(identityset.Address(31).String(), identityset.PrivateKey(28), 1, big.NewInt(1), 0, big.NewInt(0), nil)
    49  	require.NoError(t, err)
    50  	execution2, err := action.SignedExecution(identityset.Address(31).String(), identityset.PrivateKey(29), 2, big.NewInt(0), 0, big.NewInt(0), nil)
    51  	require.NoError(t, err)
    52  	execution3, err := action.SignedExecution(identityset.Address(31).String(), identityset.PrivateKey(30), 3, big.NewInt(2), 0, big.NewInt(0), nil)
    53  	require.NoError(t, err)
    54  
    55  	hash1 := hash.Hash256{}
    56  	fnv.New32().Sum(hash1[:])
    57  	blk1, err := block.NewTestingBuilder().
    58  		SetHeight(1).
    59  		SetPrevBlockHash(hash1).
    60  		SetTimeStamp(testutil.TimestampNow().UTC()).
    61  		AddActions(tsf1, tsf4, execution1).
    62  		SignAndBuild(identityset.PrivateKey(27))
    63  	require.NoError(t, err)
    64  
    65  	hash2 := hash.Hash256{}
    66  	fnv.New32().Sum(hash2[:])
    67  	blk2, err := block.NewTestingBuilder().
    68  		SetHeight(2).
    69  		SetPrevBlockHash(hash2).
    70  		SetTimeStamp(testutil.TimestampNow().UTC()).
    71  		AddActions(tsf2, tsf5, execution2).
    72  		SignAndBuild(identityset.PrivateKey(27))
    73  	require.NoError(t, err)
    74  
    75  	hash3 := hash.Hash256{}
    76  	fnv.New32().Sum(hash3[:])
    77  	blk3, err := block.NewTestingBuilder().
    78  		SetHeight(3).
    79  		SetPrevBlockHash(hash3).
    80  		SetTimeStamp(testutil.TimestampNow().UTC()).
    81  		AddActions(tsf3, tsf6, execution3).
    82  		SignAndBuild(identityset.PrivateKey(27))
    83  	require.NoError(t, err)
    84  	return []*block.Block{&blk1, &blk2, &blk3}
    85  }
    86  
    87  // TODO: Move the test to filedao. The test is not for BlockDAO, but filedao.
    88  // The current blockDAO's implementation is more about indexers and cache, which
    89  // are not covered in the unit tests.
    90  func TestBlockDAO(t *testing.T) {
    91  	require := require.New(t)
    92  
    93  	blks := getTestBlocks(t)
    94  	t1Hash, _ := blks[0].Actions[0].Hash()
    95  	t4Hash, _ := blks[0].Actions[1].Hash()
    96  	e1Hash, _ := blks[0].Actions[2].Hash()
    97  	t2Hash, _ := blks[1].Actions[0].Hash()
    98  	t5Hash, _ := blks[1].Actions[1].Hash()
    99  	e2Hash, _ := blks[1].Actions[2].Hash()
   100  	t3Hash, _ := blks[2].Actions[0].Hash()
   101  	t6Hash, _ := blks[2].Actions[1].Hash()
   102  	e3Hash, _ := blks[2].Actions[2].Hash()
   103  
   104  	addr28 := hash.BytesToHash160(identityset.Address(28).Bytes())
   105  	addr29 := hash.BytesToHash160(identityset.Address(29).Bytes())
   106  	addr30 := hash.BytesToHash160(identityset.Address(30).Bytes())
   107  	addr31 := hash.BytesToHash160(identityset.Address(31).Bytes())
   108  
   109  	type index struct {
   110  		addr   hash.Hash160
   111  		hashes [][]byte
   112  	}
   113  
   114  	daoTests := []struct {
   115  		total     uint64
   116  		hashTotal [][]byte
   117  		actions   [4]index
   118  	}{
   119  		{
   120  			9,
   121  			[][]byte{t1Hash[:], t4Hash[:], e1Hash[:], t2Hash[:], t5Hash[:], e2Hash[:], t3Hash[:], t6Hash[:], e3Hash[:]},
   122  			[4]index{
   123  				{addr28, [][]byte{t1Hash[:], t4Hash[:], e1Hash[:], t6Hash[:]}},
   124  				{addr29, [][]byte{t4Hash[:], t2Hash[:], t5Hash[:], e2Hash[:]}},
   125  				{addr30, [][]byte{t5Hash[:], t3Hash[:], t6Hash[:], e3Hash[:]}},
   126  				{addr31, [][]byte{e1Hash[:], e2Hash[:], e3Hash[:]}},
   127  			},
   128  		},
   129  		{
   130  			6,
   131  			[][]byte{t1Hash[:], t4Hash[:], e1Hash[:], t2Hash[:], t5Hash[:], e2Hash[:]},
   132  			[4]index{
   133  				{addr28, [][]byte{t1Hash[:], t4Hash[:], e1Hash[:]}},
   134  				{addr29, [][]byte{t4Hash[:], t2Hash[:], t5Hash[:], e2Hash[:]}},
   135  				{addr30, [][]byte{t5Hash[:]}},
   136  				{addr31, [][]byte{e1Hash[:], e2Hash[:]}},
   137  			},
   138  		},
   139  		{
   140  			3,
   141  			[][]byte{t1Hash[:], t4Hash[:], e1Hash[:]},
   142  			[4]index{
   143  				{addr28, [][]byte{t1Hash[:], t4Hash[:], e1Hash[:]}},
   144  				{addr29, [][]byte{t4Hash[:]}},
   145  				{addr30, nil},
   146  				{addr31, [][]byte{e1Hash[:]}},
   147  			},
   148  		},
   149  		{
   150  			0,
   151  			nil,
   152  			[4]index{
   153  				{addr28, nil},
   154  				{addr29, nil},
   155  				{addr30, nil},
   156  				{addr31, nil},
   157  			},
   158  		},
   159  	}
   160  
   161  	// receipts for the 3 blocks
   162  	receipts := [][]*action.Receipt{
   163  		{
   164  			{Status: 1, BlockHeight: 1, ActionHash: t1Hash, GasConsumed: 15, ContractAddress: "1"},
   165  			{Status: 0, BlockHeight: 1, ActionHash: t4Hash, GasConsumed: 216, ContractAddress: "2"},
   166  			{Status: 2, BlockHeight: 1, ActionHash: e1Hash, GasConsumed: 6, ContractAddress: "3"},
   167  		},
   168  		{
   169  			{Status: 3, BlockHeight: 2, ActionHash: t2Hash, GasConsumed: 1500, ContractAddress: "1"},
   170  			{Status: 5, BlockHeight: 2, ActionHash: t5Hash, GasConsumed: 34, ContractAddress: "2"},
   171  			{Status: 9, BlockHeight: 2, ActionHash: e2Hash, GasConsumed: 655, ContractAddress: "3"},
   172  		},
   173  		{
   174  			{Status: 7, BlockHeight: 3, ActionHash: t3Hash, GasConsumed: 488, ContractAddress: "1"},
   175  			{Status: 6, BlockHeight: 3, ActionHash: t6Hash, GasConsumed: 2, ContractAddress: "2"},
   176  			{Status: 2, BlockHeight: 3, ActionHash: e3Hash, GasConsumed: 1099, ContractAddress: "3"},
   177  		},
   178  	}
   179  
   180  	testBlockDao := func(dao BlockDAO, t *testing.T) {
   181  		ctx := protocol.WithBlockchainCtx(
   182  			genesis.WithGenesisContext(context.Background(), genesis.Default),
   183  			protocol.BlockchainCtx{
   184  				ChainID: 1,
   185  			})
   186  		require.NoError(dao.Start(ctx))
   187  		defer func() {
   188  			require.NoError(dao.Stop(ctx))
   189  		}()
   190  		require.True(dao.ContainsTransactionLog())
   191  
   192  		for i := 0; i < 3; i++ {
   193  			// test putBlock/Receipt
   194  			blks[i].Receipts = receipts[i]
   195  			require.NoError(dao.PutBlock(ctx, blks[i]))
   196  			blks[i].Receipts = nil
   197  			tipBlk := blks[i]
   198  
   199  			// test FileDAO's API
   200  			hash, err := dao.GetBlockHash(tipBlk.Height())
   201  			require.NoError(err)
   202  			require.Equal(tipBlk.HashBlock(), hash)
   203  			height, err := dao.GetBlockHeight(hash)
   204  			require.NoError(err)
   205  			require.Equal(tipBlk.Height(), height)
   206  			blk, err := dao.GetBlock(hash)
   207  			require.NoError(err)
   208  			require.Equal(len(blk.Actions), len(tipBlk.Actions))
   209  			for i := 0; i < len(blk.Actions); i++ {
   210  				hashVal1, hashErr1 := blk.Actions[i].Hash()
   211  				require.NoError(hashErr1)
   212  				hashVal2, hashErr2 := tipBlk.Actions[i].Hash()
   213  				require.NoError(hashErr2)
   214  				require.Equal(hashVal1, hashVal2)
   215  			}
   216  			blk, err = dao.GetBlockByHeight(height)
   217  			require.NoError(err)
   218  			require.Equal(len(blk.Actions), len(tipBlk.Actions))
   219  			for i := 0; i < len(blk.Actions); i++ {
   220  				hashVal1, hashErr1 := blk.Actions[i].Hash()
   221  				require.NoError(hashErr1)
   222  				hashVal2, hashErr2 := tipBlk.Actions[i].Hash()
   223  				require.NoError(hashErr2)
   224  				require.Equal(hashVal1, hashVal2)
   225  			}
   226  			r, err := dao.GetReceipts(height)
   227  			require.NoError(err)
   228  			require.Equal(len(receipts[i]), len(r))
   229  			for j := range receipts[i] {
   230  				b1, err := r[j].Serialize()
   231  				require.NoError(err)
   232  				b2, err := receipts[i][j].Serialize()
   233  				require.NoError(err)
   234  				require.Equal(b1, b2)
   235  			}
   236  
   237  			// test BlockDAO's API, 2nd loop to test LRU cache
   238  			for i := 0; i < 2; i++ {
   239  				header, err := dao.Header(hash)
   240  				require.NoError(err)
   241  				require.Equal(&tipBlk.Header, header)
   242  				header, err = dao.HeaderByHeight(height)
   243  				require.NoError(err)
   244  				require.Equal(&tipBlk.Header, header)
   245  				footer, err := dao.FooterByHeight(height)
   246  				require.NoError(err)
   247  				require.Equal(&tipBlk.Footer, footer)
   248  			}
   249  		}
   250  
   251  		height, err := dao.Height()
   252  		require.NoError(err)
   253  		require.EqualValues(len(blks), height)
   254  
   255  		// commit an existing block
   256  		require.Equal(filedao.ErrAlreadyExist, dao.PutBlock(ctx, blks[2]))
   257  
   258  		// check non-exist block
   259  		h, err := dao.GetBlockHash(5)
   260  		require.Equal(db.ErrNotExist, errors.Cause(err))
   261  		require.Equal(hash.ZeroHash256, h)
   262  		height, err = dao.GetBlockHeight(hash.ZeroHash256)
   263  		require.Equal(db.ErrNotExist, errors.Cause(err))
   264  		require.EqualValues(0, height)
   265  		blk, err := dao.GetBlock(hash.ZeroHash256)
   266  		require.Equal(db.ErrNotExist, errors.Cause(err))
   267  		require.Nil(blk)
   268  		blk, err = dao.GetBlockByHeight(5)
   269  		require.Equal(db.ErrNotExist, errors.Cause(err))
   270  		require.Nil(blk)
   271  		r, err := dao.GetReceipts(5)
   272  		require.Equal(db.ErrNotExist, errors.Cause(err))
   273  		require.Nil(r)
   274  
   275  		// Test GetReceipt/ActionByActionHash
   276  		for i, v := range daoTests[0].hashTotal {
   277  			blk := blks[i/3]
   278  			h := hash.BytesToHash256(v)
   279  			receipt, err := receiptByActionHash(receipts[i/3], h)
   280  			require.NoError(err)
   281  			b1, err := receipt.Serialize()
   282  			require.NoError(err)
   283  			b2, err := receipts[i/3][i%3].Serialize()
   284  			require.NoError(err)
   285  			require.Equal(b1, b2)
   286  			action, actIndex, err := blk.ActionByHash(h)
   287  			require.NoError(err)
   288  			require.Equal(int(actIndex), i%3)
   289  			require.Equal(blk.Actions[i%3], action)
   290  		}
   291  	}
   292  
   293  	testDeleteDao := func(dao filedao.FileDAO, t *testing.T) {
   294  		ctx := protocol.WithBlockchainCtx(
   295  			genesis.WithGenesisContext(context.Background(), genesis.Default),
   296  			protocol.BlockchainCtx{
   297  				ChainID: 1,
   298  			})
   299  		require.NoError(dao.Start(ctx))
   300  		defer func() {
   301  			require.NoError(dao.Stop(ctx))
   302  		}()
   303  
   304  		// put blocks
   305  		for i := 0; i < 3; i++ {
   306  			blks[i].Receipts = receipts[i]
   307  			require.NoError(dao.PutBlock(ctx, blks[i]))
   308  			blks[i].Receipts = nil
   309  		}
   310  
   311  		// delete tip block one by one, verify address/action after each deletion
   312  		for i, action := range daoTests {
   313  			if i == 0 {
   314  				// tests[0] is the whole address/action data at block height 3
   315  				continue
   316  			}
   317  			prevTipHeight, err := dao.Height()
   318  			require.NoError(err)
   319  			prevTipHash, err := dao.GetBlockHash(prevTipHeight)
   320  			require.NoError(err)
   321  			for {
   322  				height, err := dao.Height()
   323  				require.NoError(err)
   324  				if height <= prevTipHeight-1 {
   325  					break
   326  				}
   327  				require.NoError(dao.DeleteTipBlock())
   328  			}
   329  			tipHeight, err := dao.Height()
   330  			require.NoError(err)
   331  			require.EqualValues(prevTipHeight-1, tipHeight)
   332  			_, err = dao.GetBlockHash(prevTipHeight)
   333  			require.Error(err)
   334  			_, err = dao.GetBlockHeight(prevTipHash)
   335  			require.Error(err)
   336  
   337  			if tipHeight == 0 {
   338  				h, err := dao.GetBlockHash(0)
   339  				require.NoError(err)
   340  				require.Equal(block.GenesisHash(), h)
   341  				continue
   342  			}
   343  			tipBlk := blks[tipHeight-1]
   344  			require.Equal(tipBlk.Height(), tipHeight)
   345  
   346  			// test FileDAO's API
   347  			h, err := dao.GetBlockHash(tipHeight)
   348  			require.NoError(err)
   349  			require.Equal(tipBlk.HashBlock(), h)
   350  			height, err := dao.GetBlockHeight(h)
   351  			require.NoError(err)
   352  			require.Equal(tipHeight, height)
   353  			blk, err := dao.GetBlock(h)
   354  			require.NoError(err)
   355  			require.Equal(len(blk.Actions), len(tipBlk.Actions))
   356  			for i := 0; i < len(blk.Actions); i++ {
   357  				hashVal1, hashErr1 := blk.Actions[i].Hash()
   358  				require.NoError(hashErr1)
   359  				hashVal2, hashErr2 := tipBlk.Actions[i].Hash()
   360  				require.NoError(hashErr2)
   361  				require.Equal(hashVal1, hashVal2)
   362  			}
   363  			blk, err = dao.GetBlockByHeight(height)
   364  			require.NoError(err)
   365  			require.Equal(len(blk.Actions), len(tipBlk.Actions))
   366  			for i := 0; i < len(blk.Actions); i++ {
   367  				hashVal1, hashErr1 := blk.Actions[i].Hash()
   368  				require.NoError(hashErr1)
   369  				hashVal2, hashErr2 := tipBlk.Actions[i].Hash()
   370  				require.NoError(hashErr2)
   371  				require.Equal(hashVal1, hashVal2)
   372  			}
   373  
   374  			// test BlockDAO's API, 2nd loop to test LRU cache
   375  			for i := 0; i < 2; i++ {
   376  				header, err := dao.Header(h)
   377  				require.NoError(err)
   378  				require.Equal(&tipBlk.Header, header)
   379  				header, err = dao.HeaderByHeight(height)
   380  				require.NoError(err)
   381  				require.Equal(&tipBlk.Header, header)
   382  				footer, err := dao.FooterByHeight(height)
   383  				require.NoError(err)
   384  				require.Equal(&tipBlk.Footer, footer)
   385  			}
   386  
   387  			// Test GetReceipt/ActionByActionHash
   388  			for i, v := range action.hashTotal {
   389  				blk := blks[i/3]
   390  				h := hash.BytesToHash256(v)
   391  				receipt, err := receiptByActionHash(receipts[i/3], h)
   392  				require.NoError(err)
   393  				b1, err := receipt.Serialize()
   394  				require.NoError(err)
   395  				b2, err := receipts[i/3][i%3].Serialize()
   396  				require.NoError(err)
   397  				require.Equal(b1, b2)
   398  				action, actIndex, err := blk.ActionByHash(h)
   399  				require.NoError(err)
   400  				require.Equal(int(actIndex), i%3)
   401  				require.Equal(blk.Actions[i%3], action)
   402  			}
   403  		}
   404  	}
   405  
   406  	testPath, err := testutil.PathOfTempFile("test-kv-store")
   407  	require.NoError(err)
   408  	defer func() {
   409  		testutil.CleanupPath(testPath)
   410  	}()
   411  
   412  	daoList := []struct {
   413  		inMemory, legacy bool
   414  		compressBlock    string
   415  	}{
   416  		{true, false, ""},
   417  		{false, true, ""},
   418  		{false, true, compress.Gzip},
   419  		{false, false, ""},
   420  		{false, false, compress.Gzip},
   421  		{false, false, compress.Snappy},
   422  	}
   423  
   424  	cfg := db.DefaultConfig
   425  	cfg.DbPath = testPath
   426  	genesis.SetGenesisTimestamp(genesis.Default.Timestamp)
   427  	block.LoadGenesisHash(&genesis.Default)
   428  	for _, v := range daoList {
   429  		testutil.CleanupPath(testPath)
   430  		dao, err := createFileDAO(v.inMemory, v.legacy, v.compressBlock, cfg)
   431  		require.NoError(err)
   432  		require.NotNil(dao)
   433  		t.Run("test store blocks", func(t *testing.T) {
   434  			testBlockDao(dao, t)
   435  		})
   436  	}
   437  
   438  	for _, v := range daoList {
   439  		testutil.CleanupPath(testPath)
   440  		dao, err := createFileDAO(v.inMemory, v.legacy, v.compressBlock, cfg)
   441  		require.NoError(err)
   442  		require.NotNil(dao)
   443  		t.Run("test delete blocks", func(t *testing.T) {
   444  			testDeleteDao(dao, t)
   445  		})
   446  	}
   447  }
   448  
   449  func createFileDAO(inMemory, legacy bool, compressBlock string, cfg db.Config) (filedao.FileDAO, error) {
   450  	if inMemory {
   451  		return filedao.NewFileDAOInMemForTest()
   452  	}
   453  	deser := block.NewDeserializer(4689)
   454  	if legacy {
   455  		return filedao.CreateFileDAO(true, cfg, deser)
   456  	}
   457  
   458  	cfg.Compressor = compressBlock
   459  	return filedao.NewFileDAO(cfg, deser)
   460  }
   461  
   462  func BenchmarkBlockCache(b *testing.B) {
   463  	test := func(cacheSize int, b *testing.B) {
   464  		b.StopTimer()
   465  		path := "test-kv-store"
   466  		testPath, err := testutil.PathOfTempFile(path)
   467  		require.NoError(b, err)
   468  		indexPath, err := testutil.PathOfTempFile(path)
   469  		require.NoError(b, err)
   470  		cfg := db.Config{
   471  			NumRetries: 1,
   472  		}
   473  		defer func() {
   474  			testutil.CleanupPath(testPath)
   475  			testutil.CleanupPath(indexPath)
   476  		}()
   477  		cfg.DbPath = indexPath
   478  		cfg.DbPath = testPath
   479  		cfg.MaxCacheSize = cacheSize
   480  		deser := block.NewDeserializer(4689)
   481  		fileDAO, err := filedao.NewFileDAO(cfg, deser)
   482  		require.NoError(b, err)
   483  		blkDao := NewBlockDAOWithIndexersAndCache(fileDAO, []BlockIndexer{}, cfg.MaxCacheSize)
   484  		require.NoError(b, blkDao.Start(context.Background()))
   485  		defer func() {
   486  			require.NoError(b, blkDao.Stop(context.Background()))
   487  		}()
   488  		prevHash := hash.ZeroHash256
   489  		numBlks := 8640
   490  		for i := 1; i <= numBlks; i++ {
   491  			actions := make([]*action.SealedEnvelope, 10)
   492  			for j := 0; j < 10; j++ {
   493  				actions[j], err = action.SignedTransfer(
   494  					identityset.Address(j).String(),
   495  					identityset.PrivateKey(j+1),
   496  					1,
   497  					unit.ConvertIotxToRau(1),
   498  					nil,
   499  					testutil.TestGasLimit,
   500  					testutil.TestGasPrice,
   501  				)
   502  				require.NoError(b, err)
   503  			}
   504  			tb := block.TestingBuilder{}
   505  			blk, err := tb.SetPrevBlockHash(prevHash).
   506  				SetVersion(1).
   507  				SetTimeStamp(time.Now()).
   508  				SetHeight(uint64(i)).
   509  				AddActions(actions...).
   510  				SignAndBuild(identityset.PrivateKey(0))
   511  			require.NoError(b, err)
   512  			err = blkDao.PutBlock(context.Background(), &blk)
   513  			require.NoError(b, err)
   514  			prevHash = blk.HashBlock()
   515  		}
   516  		b.ResetTimer()
   517  	}
   518  	b.Run("cache", func(b *testing.B) {
   519  		test(8640, b)
   520  	})
   521  	b.Run("no-cache", func(b *testing.B) {
   522  		test(0, b)
   523  	})
   524  }
   525  
   526  func receiptByActionHash(receipts []*action.Receipt, h hash.Hash256) (*action.Receipt, error) {
   527  	for _, r := range receipts {
   528  		if r.ActionHash == h {
   529  			return r, nil
   530  		}
   531  	}
   532  	return nil, errors.Errorf("receipt of action %x isn't found", h)
   533  }