github.com/koko1123/flow-go-1@v0.29.6/engine/execution/execution_test.go (about)

     1  package execution_test
     2  
     3  import (
     4  	"context"
     5  	"sync"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/rs/zerolog/log"
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/mock"
    12  	"github.com/stretchr/testify/require"
    13  	"github.com/vmihailenco/msgpack"
    14  	"go.uber.org/atomic"
    15  
    16  	execTestutil "github.com/koko1123/flow-go-1/engine/execution/testutil"
    17  	"github.com/koko1123/flow-go-1/engine/testutil"
    18  	testmock "github.com/koko1123/flow-go-1/engine/testutil/mock"
    19  	"github.com/koko1123/flow-go-1/model/flow"
    20  	"github.com/koko1123/flow-go-1/model/flow/order"
    21  	"github.com/koko1123/flow-go-1/model/messages"
    22  	"github.com/koko1123/flow-go-1/module/signature"
    23  	"github.com/koko1123/flow-go-1/network/channels"
    24  	"github.com/koko1123/flow-go-1/network/mocknetwork"
    25  	"github.com/koko1123/flow-go-1/network/stub"
    26  	"github.com/koko1123/flow-go-1/state/cluster"
    27  	"github.com/koko1123/flow-go-1/utils/unittest"
    28  )
    29  
    30  func sendBlock(exeNode *testmock.ExecutionNode, from flow.Identifier, proposal *messages.BlockProposal) error {
    31  	return exeNode.FollowerEngine.Process(channels.ReceiveBlocks, from, proposal)
    32  }
    33  
    34  // Test when the ingestion engine receives a block, it will
    35  // request collections from collection node, and send ER to
    36  // verification node and consensus node.
    37  // create a block that has two collections: col1 and col2;
    38  // col1 has tx1 and tx2, col2 has tx3 and tx4.
    39  // create another child block which will trigger the parent
    40  // block to be incorporated and be passed to the ingestion engine
    41  func TestExecutionFlow(t *testing.T) {
    42  	hub := stub.NewNetworkHub()
    43  
    44  	chainID := flow.Testnet
    45  
    46  	colID := unittest.IdentityFixture(
    47  		unittest.WithRole(flow.RoleCollection),
    48  		unittest.WithKeys,
    49  	)
    50  	conID := unittest.IdentityFixture(
    51  		unittest.WithRole(flow.RoleConsensus),
    52  		unittest.WithKeys,
    53  	)
    54  	exeID := unittest.IdentityFixture(
    55  		unittest.WithRole(flow.RoleExecution),
    56  		unittest.WithKeys,
    57  	)
    58  	verID := unittest.IdentityFixture(
    59  		unittest.WithRole(flow.RoleVerification),
    60  		unittest.WithKeys,
    61  	)
    62  
    63  	identities := unittest.CompleteIdentitySet(colID, conID, exeID, verID).Sort(order.Canonical)
    64  
    65  	// create execution node
    66  	exeNode := testutil.ExecutionNode(t, hub, exeID, identities, 21, chainID)
    67  
    68  	ctx, cancel := context.WithCancel(context.Background())
    69  	unittest.RequireReturnsBefore(t, func() {
    70  		exeNode.Ready(ctx)
    71  	}, 1*time.Second, "could not start execution node on time")
    72  	defer exeNode.Done(cancel)
    73  
    74  	genesis, err := exeNode.State.AtHeight(0).Head()
    75  	require.NoError(t, err)
    76  
    77  	tx1 := flow.TransactionBody{
    78  		Script: []byte("transaction { execute { log(1) } }"),
    79  	}
    80  
    81  	tx2 := flow.TransactionBody{
    82  		Script: []byte("transaction { execute { log(2) } }"),
    83  	}
    84  
    85  	tx3 := flow.TransactionBody{
    86  		Script: []byte("transaction { execute { log(3) } }"),
    87  	}
    88  
    89  	tx4 := flow.TransactionBody{
    90  		Script: []byte("transaction { execute { log(4) } }"),
    91  	}
    92  
    93  	col1 := flow.Collection{Transactions: []*flow.TransactionBody{&tx1, &tx2}}
    94  	col2 := flow.Collection{Transactions: []*flow.TransactionBody{&tx3, &tx4}}
    95  
    96  	collections := map[flow.Identifier]*flow.Collection{
    97  		col1.ID(): &col1,
    98  		col2.ID(): &col2,
    99  	}
   100  
   101  	clusterChainID := cluster.CanonicalClusterID(1, flow.IdentityList{colID})
   102  
   103  	// signed by the only collector
   104  	block := unittest.BlockWithParentAndProposerFixture(genesis, conID.NodeID, 1)
   105  	voterIndices, err := signature.EncodeSignersToIndices(
   106  		[]flow.Identifier{conID.NodeID}, []flow.Identifier{conID.NodeID})
   107  	require.NoError(t, err)
   108  	block.Header.ParentVoterIndices = voterIndices
   109  	signerIndices, err := signature.EncodeSignersToIndices(
   110  		[]flow.Identifier{colID.NodeID}, []flow.Identifier{colID.NodeID})
   111  	require.NoError(t, err)
   112  	block.SetPayload(flow.Payload{
   113  		Guarantees: []*flow.CollectionGuarantee{
   114  			{
   115  				CollectionID:     col1.ID(),
   116  				SignerIndices:    signerIndices,
   117  				ChainID:          clusterChainID,
   118  				ReferenceBlockID: genesis.ID(),
   119  			},
   120  			{
   121  				CollectionID:     col2.ID(),
   122  				SignerIndices:    signerIndices,
   123  				ChainID:          clusterChainID,
   124  				ReferenceBlockID: genesis.ID(),
   125  			},
   126  		},
   127  	})
   128  
   129  	child := unittest.BlockWithParentAndProposerFixture(block.Header, conID.NodeID, 1)
   130  	// the default signer indices is 2 bytes, but in this test cases
   131  	// we need 1 byte
   132  	child.Header.ParentVoterIndices = voterIndices
   133  
   134  	log.Info().Msgf("child block ID: %v, indices: %x", child.Header.ID(), child.Header.ParentVoterIndices)
   135  
   136  	collectionNode := testutil.GenericNodeFromParticipants(t, hub, colID, identities, chainID)
   137  	defer collectionNode.Done()
   138  	verificationNode := testutil.GenericNodeFromParticipants(t, hub, verID, identities, chainID)
   139  	defer verificationNode.Done()
   140  	consensusNode := testutil.GenericNodeFromParticipants(t, hub, conID, identities, chainID)
   141  	defer consensusNode.Done()
   142  
   143  	// create collection node that can respond collections to execution node
   144  	// check collection node received the collection request from execution node
   145  	providerEngine := new(mocknetwork.Engine)
   146  	provConduit, _ := collectionNode.Net.Register(channels.ProvideCollections, providerEngine)
   147  	providerEngine.On("Process", mock.AnythingOfType("channels.Channel"), exeID.NodeID, mock.Anything).
   148  		Run(func(args mock.Arguments) {
   149  			originID := args.Get(1).(flow.Identifier)
   150  			req := args.Get(2).(*messages.EntityRequest)
   151  
   152  			var entities []flow.Entity
   153  			for _, entityID := range req.EntityIDs {
   154  				coll, exists := collections[entityID]
   155  				require.True(t, exists)
   156  				entities = append(entities, coll)
   157  			}
   158  
   159  			var blobs [][]byte
   160  			for _, entity := range entities {
   161  				blob, _ := msgpack.Marshal(entity)
   162  				blobs = append(blobs, blob)
   163  			}
   164  
   165  			res := &messages.EntityResponse{
   166  				Nonce:     req.Nonce,
   167  				EntityIDs: req.EntityIDs,
   168  				Blobs:     blobs,
   169  			}
   170  
   171  			err := provConduit.Publish(res, originID)
   172  			assert.NoError(t, err)
   173  		}).
   174  		Once().
   175  		Return(nil)
   176  
   177  	var lock sync.Mutex
   178  	var receipt *flow.ExecutionReceipt
   179  
   180  	// create verification engine that can create approvals and send to consensus nodes
   181  	// check the verification engine received the ER from execution node
   182  	verificationEngine := new(mocknetwork.Engine)
   183  	_, _ = verificationNode.Net.Register(channels.ReceiveReceipts, verificationEngine)
   184  	verificationEngine.On("Process", mock.AnythingOfType("channels.Channel"), exeID.NodeID, mock.Anything).
   185  		Run(func(args mock.Arguments) {
   186  			lock.Lock()
   187  			defer lock.Unlock()
   188  			receipt, _ = args[2].(*flow.ExecutionReceipt)
   189  
   190  			assert.Equal(t, block.ID(), receipt.ExecutionResult.BlockID)
   191  		}).
   192  		Return(nil).
   193  		Once()
   194  
   195  	// create consensus engine that accepts the result
   196  	// check the consensus engine has received the result from execution node
   197  	consensusEngine := new(mocknetwork.Engine)
   198  	_, _ = consensusNode.Net.Register(channels.ReceiveReceipts, consensusEngine)
   199  	consensusEngine.On("Process", mock.AnythingOfType("channels.Channel"), exeID.NodeID, mock.Anything).
   200  		Run(func(args mock.Arguments) {
   201  			lock.Lock()
   202  			defer lock.Unlock()
   203  
   204  			receipt, _ = args[2].(*flow.ExecutionReceipt)
   205  
   206  			assert.Equal(t, block.ID(), receipt.ExecutionResult.BlockID)
   207  			assert.Equal(t, len(collections), len(receipt.ExecutionResult.Chunks)-1) // don't count system chunk
   208  
   209  			for i, chunk := range receipt.ExecutionResult.Chunks {
   210  				assert.EqualValues(t, i, chunk.CollectionIndex)
   211  			}
   212  		}).
   213  		Return(nil).
   214  		Once()
   215  
   216  	// submit block from consensus node
   217  	err = sendBlock(&exeNode, conID.NodeID, unittest.ProposalFromBlock(&block))
   218  	require.NoError(t, err)
   219  
   220  	// submit the child block from consensus node, which trigger the parent block
   221  	// to be passed to BlockProcessable
   222  	err = sendBlock(&exeNode, conID.NodeID, unittest.ProposalFromBlock(&child))
   223  	require.NoError(t, err)
   224  
   225  	require.Eventually(t, func() bool {
   226  		// when sendBlock returned, ingestion engine might not have processed
   227  		// the block yet, because the process is async. we have to wait
   228  		hub.DeliverAll()
   229  
   230  		lock.Lock()
   231  		defer lock.Unlock()
   232  		return receipt != nil
   233  	}, time.Second*10, time.Millisecond*500)
   234  
   235  	// check that the block has been executed.
   236  	exeNode.AssertHighestExecutedBlock(t, block.Header)
   237  
   238  	myReceipt, err := exeNode.MyExecutionReceipts.MyReceipt(block.ID())
   239  	require.NoError(t, err)
   240  	require.NotNil(t, myReceipt)
   241  	require.Equal(t, exeNode.Me.NodeID(), myReceipt.ExecutorID)
   242  
   243  	providerEngine.AssertExpectations(t)
   244  	verificationEngine.AssertExpectations(t)
   245  	consensusEngine.AssertExpectations(t)
   246  }
   247  
   248  func deployContractBlock(t *testing.T, conID *flow.Identity, colID *flow.Identity, chain flow.Chain, seq uint64, parent *flow.Header, ref *flow.Header) (
   249  	*flow.TransactionBody, *flow.Collection, flow.Block, *messages.BlockProposal, uint64) {
   250  	// make tx
   251  	tx := execTestutil.DeployCounterContractTransaction(chain.ServiceAddress(), chain)
   252  	err := execTestutil.SignTransactionAsServiceAccount(tx, seq, chain)
   253  	require.NoError(t, err)
   254  
   255  	// make collection
   256  	col := &flow.Collection{Transactions: []*flow.TransactionBody{tx}}
   257  
   258  	signerIndices, err := signature.EncodeSignersToIndices(
   259  		[]flow.Identifier{colID.NodeID}, []flow.Identifier{colID.NodeID})
   260  	require.NoError(t, err)
   261  
   262  	clusterChainID := cluster.CanonicalClusterID(1, flow.IdentityList{colID})
   263  
   264  	// make block
   265  	block := unittest.BlockWithParentAndProposerFixture(parent, conID.NodeID, 1)
   266  	voterIndices, err := signature.EncodeSignersToIndices(
   267  		[]flow.Identifier{conID.NodeID}, []flow.Identifier{conID.NodeID})
   268  	require.NoError(t, err)
   269  	block.Header.ParentVoterIndices = voterIndices
   270  	block.SetPayload(flow.Payload{
   271  		Guarantees: []*flow.CollectionGuarantee{
   272  			{
   273  				CollectionID:     col.ID(),
   274  				SignerIndices:    signerIndices,
   275  				ChainID:          clusterChainID,
   276  				ReferenceBlockID: ref.ID(),
   277  			},
   278  		},
   279  	})
   280  
   281  	// make proposal
   282  	proposal := unittest.ProposalFromBlock(&block)
   283  
   284  	return tx, col, block, proposal, seq + 1
   285  }
   286  
   287  func makePanicBlock(t *testing.T, conID *flow.Identity, colID *flow.Identity, chain flow.Chain, seq uint64, parent *flow.Header, ref *flow.Header) (
   288  	*flow.TransactionBody, *flow.Collection, flow.Block, *messages.BlockProposal, uint64) {
   289  	// make tx
   290  	tx := execTestutil.CreateCounterPanicTransaction(chain.ServiceAddress(), chain.ServiceAddress())
   291  	err := execTestutil.SignTransactionAsServiceAccount(tx, seq, chain)
   292  	require.NoError(t, err)
   293  
   294  	// make collection
   295  	col := &flow.Collection{Transactions: []*flow.TransactionBody{tx}}
   296  
   297  	clusterChainID := cluster.CanonicalClusterID(1, flow.IdentityList{colID})
   298  	// make block
   299  	block := unittest.BlockWithParentAndProposerFixture(parent, conID.NodeID, 1)
   300  	voterIndices, err := signature.EncodeSignersToIndices(
   301  		[]flow.Identifier{conID.NodeID}, []flow.Identifier{conID.NodeID})
   302  	require.NoError(t, err)
   303  	block.Header.ParentVoterIndices = voterIndices
   304  
   305  	signerIndices, err := signature.EncodeSignersToIndices(
   306  		[]flow.Identifier{colID.NodeID}, []flow.Identifier{colID.NodeID})
   307  	require.NoError(t, err)
   308  
   309  	block.SetPayload(flow.Payload{
   310  		Guarantees: []*flow.CollectionGuarantee{
   311  			{CollectionID: col.ID(), SignerIndices: signerIndices, ChainID: clusterChainID, ReferenceBlockID: ref.ID()},
   312  		},
   313  	})
   314  
   315  	proposal := unittest.ProposalFromBlock(&block)
   316  
   317  	return tx, col, block, proposal, seq + 1
   318  }
   319  
   320  func makeSuccessBlock(t *testing.T, conID *flow.Identity, colID *flow.Identity, chain flow.Chain, seq uint64, parent *flow.Header, ref *flow.Header) (
   321  	*flow.TransactionBody, *flow.Collection, flow.Block, *messages.BlockProposal, uint64) {
   322  	tx := execTestutil.AddToCounterTransaction(chain.ServiceAddress(), chain.ServiceAddress())
   323  	err := execTestutil.SignTransactionAsServiceAccount(tx, seq, chain)
   324  	require.NoError(t, err)
   325  
   326  	signerIndices, err := signature.EncodeSignersToIndices(
   327  		[]flow.Identifier{colID.NodeID}, []flow.Identifier{colID.NodeID})
   328  	require.NoError(t, err)
   329  	clusterChainID := cluster.CanonicalClusterID(1, flow.IdentityList{colID})
   330  
   331  	col := &flow.Collection{Transactions: []*flow.TransactionBody{tx}}
   332  	block := unittest.BlockWithParentAndProposerFixture(parent, conID.NodeID, 1)
   333  	voterIndices, err := signature.EncodeSignersToIndices(
   334  		[]flow.Identifier{conID.NodeID}, []flow.Identifier{conID.NodeID})
   335  	require.NoError(t, err)
   336  	block.Header.ParentVoterIndices = voterIndices
   337  	block.SetPayload(flow.Payload{
   338  		Guarantees: []*flow.CollectionGuarantee{
   339  			{CollectionID: col.ID(), SignerIndices: signerIndices, ChainID: clusterChainID, ReferenceBlockID: ref.ID()},
   340  		},
   341  	})
   342  
   343  	proposal := unittest.ProposalFromBlock(&block)
   344  
   345  	return tx, col, block, proposal, seq + 1
   346  }
   347  
   348  // Test a successful tx should change the statecommitment,
   349  // but a failed Tx should not change the statecommitment.
   350  func TestFailedTxWillNotChangeStateCommitment(t *testing.T) {
   351  	hub := stub.NewNetworkHub()
   352  
   353  	chainID := flow.Emulator
   354  
   355  	colID := unittest.IdentityFixture(
   356  		unittest.WithRole(flow.RoleCollection),
   357  		unittest.WithKeys,
   358  	)
   359  	conID := unittest.IdentityFixture(
   360  		unittest.WithRole(flow.RoleConsensus),
   361  		unittest.WithKeys,
   362  	)
   363  	exe1ID := unittest.IdentityFixture(
   364  		unittest.WithRole(flow.RoleExecution),
   365  		unittest.WithKeys,
   366  	)
   367  
   368  	identities := unittest.CompleteIdentitySet(colID, conID, exe1ID)
   369  	key := unittest.NetworkingPrivKeyFixture()
   370  	identities[3].NetworkPubKey = key.PublicKey()
   371  
   372  	collectionNode := testutil.GenericNodeFromParticipants(t, hub, colID, identities, chainID)
   373  	defer collectionNode.Done()
   374  	consensusNode := testutil.GenericNodeFromParticipants(t, hub, conID, identities, chainID)
   375  	defer consensusNode.Done()
   376  	exe1Node := testutil.ExecutionNode(t, hub, exe1ID, identities, 27, chainID)
   377  
   378  	ctx, cancel := context.WithCancel(context.Background())
   379  	unittest.RequireReturnsBefore(t, func() {
   380  		exe1Node.Ready(ctx)
   381  	}, 1*time.Second, "could not start execution node on time")
   382  	defer exe1Node.Done(cancel)
   383  
   384  	genesis, err := exe1Node.State.AtHeight(0).Head()
   385  	require.NoError(t, err)
   386  
   387  	seq := uint64(0)
   388  
   389  	chain := exe1Node.ChainID.Chain()
   390  
   391  	// transaction that will change state and succeed, used to test that state commitment changes
   392  	// genesis <- block1 [tx1] <- block2 [tx2] <- block3 [tx3] <- child
   393  	_, col1, block1, proposal1, seq := deployContractBlock(t, conID, colID, chain, seq, genesis, genesis)
   394  
   395  	// we don't set the proper sequence number of this one
   396  	_, col2, block2, proposal2, _ := makePanicBlock(t, conID, colID, chain, uint64(0), block1.Header, genesis)
   397  
   398  	_, col3, block3, proposal3, seq := makeSuccessBlock(t, conID, colID, chain, seq, block2.Header, genesis)
   399  
   400  	_, _, _, proposal4, _ := makeSuccessBlock(t, conID, colID, chain, seq, block3.Header, genesis)
   401  	// seq++
   402  
   403  	// setup mocks and assertions
   404  	collectionEngine := mockCollectionEngineToReturnCollections(
   405  		t,
   406  		&collectionNode,
   407  		[]*flow.Collection{col1, col2, col3},
   408  	)
   409  
   410  	receiptsReceived := atomic.Uint64{}
   411  
   412  	consensusEngine := new(mocknetwork.Engine)
   413  	_, _ = consensusNode.Net.Register(channels.ReceiveReceipts, consensusEngine)
   414  	consensusEngine.On("Process", mock.AnythingOfType("channels.Channel"), mock.Anything, mock.Anything).
   415  		Run(func(args mock.Arguments) {
   416  			receiptsReceived.Inc()
   417  			originID := args[1].(flow.Identifier)
   418  			receipt := args[2].(*flow.ExecutionReceipt)
   419  			finalState, _ := receipt.ExecutionResult.FinalStateCommitment()
   420  			consensusNode.Log.Debug().
   421  				Hex("origin", originID[:]).
   422  				Hex("block", receipt.ExecutionResult.BlockID[:]).
   423  				Hex("final_state_commit", finalState[:]).
   424  				Msg("execution receipt delivered")
   425  		}).Return(nil)
   426  
   427  	// submit block2 from consensus node to execution node 1
   428  	err = sendBlock(&exe1Node, conID.NodeID, proposal1)
   429  	require.NoError(t, err)
   430  
   431  	err = sendBlock(&exe1Node, conID.NodeID, proposal2)
   432  	assert.NoError(t, err)
   433  
   434  	// ensure block 1 has been executed
   435  	hub.DeliverAllEventually(t, func() bool {
   436  		return receiptsReceived.Load() == 1
   437  	})
   438  	exe1Node.AssertHighestExecutedBlock(t, block1.Header)
   439  
   440  	scExe1Genesis, err := exe1Node.ExecutionState.StateCommitmentByBlockID(context.Background(), genesis.ID())
   441  	assert.NoError(t, err)
   442  
   443  	scExe1Block1, err := exe1Node.ExecutionState.StateCommitmentByBlockID(context.Background(), block1.ID())
   444  	assert.NoError(t, err)
   445  	assert.NotEqual(t, scExe1Genesis, scExe1Block1)
   446  
   447  	// submit block 3 and block 4 from consensus node to execution node 1 (who have block1),
   448  	err = sendBlock(&exe1Node, conID.NodeID, proposal3)
   449  	assert.NoError(t, err)
   450  
   451  	err = sendBlock(&exe1Node, conID.NodeID, proposal4)
   452  	assert.NoError(t, err)
   453  
   454  	// ensure block 1, 2 and 3 have been executed
   455  	hub.DeliverAllEventually(t, func() bool {
   456  		return receiptsReceived.Load() == 3
   457  	})
   458  
   459  	// ensure state has been synced across both nodes
   460  	exe1Node.AssertHighestExecutedBlock(t, block3.Header)
   461  	// exe2Node.AssertHighestExecutedBlock(t, block3.Header)
   462  
   463  	// verify state commitment of block 2 is the same as block 1, since tx failed on seq number verification
   464  	scExe1Block2, err := exe1Node.ExecutionState.StateCommitmentByBlockID(context.Background(), block2.ID())
   465  	assert.NoError(t, err)
   466  	// TODO this is no longer valid because the system chunk can change the state
   467  	//assert.Equal(t, scExe1Block1, scExe1Block2)
   468  	_ = scExe1Block2
   469  
   470  	collectionEngine.AssertExpectations(t)
   471  	consensusEngine.AssertExpectations(t)
   472  }
   473  
   474  func mockCollectionEngineToReturnCollections(t *testing.T, collectionNode *testmock.GenericNode, cols []*flow.Collection) *mocknetwork.Engine {
   475  	collectionEngine := new(mocknetwork.Engine)
   476  	colConduit, _ := collectionNode.Net.Register(channels.RequestCollections, collectionEngine)
   477  
   478  	// make lookup
   479  	colMap := make(map[flow.Identifier][]byte)
   480  	for _, col := range cols {
   481  		blob, _ := msgpack.Marshal(col)
   482  		colMap[col.ID()] = blob
   483  	}
   484  	collectionEngine.On("Process", mock.AnythingOfType("channels.Channel"), mock.Anything, mock.Anything).
   485  		Run(func(args mock.Arguments) {
   486  			originID := args[1].(flow.Identifier)
   487  			req := args[2].(*messages.EntityRequest)
   488  			blob, ok := colMap[req.EntityIDs[0]]
   489  			if !ok {
   490  				assert.FailNow(t, "requesting unexpected collection", req.EntityIDs[0])
   491  			}
   492  			res := &messages.EntityResponse{Blobs: [][]byte{blob}, EntityIDs: req.EntityIDs[:1]}
   493  			err := colConduit.Publish(res, originID)
   494  			assert.NoError(t, err)
   495  		}).
   496  		Return(nil).
   497  		Times(len(cols))
   498  	return collectionEngine
   499  }
   500  
   501  // Test the receipt will be sent to multiple verification nodes
   502  func TestBroadcastToMultipleVerificationNodes(t *testing.T) {
   503  	hub := stub.NewNetworkHub()
   504  
   505  	chainID := flow.Emulator
   506  
   507  	colID := unittest.IdentityFixture(
   508  		unittest.WithRole(flow.RoleCollection),
   509  		unittest.WithKeys,
   510  	)
   511  	conID := unittest.IdentityFixture(
   512  		unittest.WithRole(flow.RoleConsensus),
   513  		unittest.WithKeys,
   514  	)
   515  	exeID := unittest.IdentityFixture(
   516  		unittest.WithRole(flow.RoleExecution),
   517  		unittest.WithKeys,
   518  	)
   519  	ver1ID := unittest.IdentityFixture(
   520  		unittest.WithRole(flow.RoleVerification),
   521  		unittest.WithKeys,
   522  	)
   523  	ver2ID := unittest.IdentityFixture(
   524  		unittest.WithRole(flow.RoleVerification),
   525  		unittest.WithKeys,
   526  	)
   527  
   528  	identities := unittest.CompleteIdentitySet(colID, conID, exeID, ver1ID, ver2ID)
   529  
   530  	exeNode := testutil.ExecutionNode(t, hub, exeID, identities, 21, chainID)
   531  	ctx, cancel := context.WithCancel(context.Background())
   532  
   533  	unittest.RequireReturnsBefore(t, func() {
   534  		exeNode.Ready(ctx)
   535  	}, 1*time.Second, "could not start execution node on time")
   536  	defer exeNode.Done(cancel)
   537  
   538  	verification1Node := testutil.GenericNodeFromParticipants(t, hub, ver1ID, identities, chainID)
   539  	defer verification1Node.Done()
   540  	verification2Node := testutil.GenericNodeFromParticipants(t, hub, ver2ID, identities, chainID)
   541  	defer verification2Node.Done()
   542  
   543  	genesis, err := exeNode.State.AtHeight(0).Head()
   544  	require.NoError(t, err)
   545  
   546  	block := unittest.BlockWithParentAndProposerFixture(genesis, conID.NodeID, 1)
   547  	voterIndices, err := signature.EncodeSignersToIndices(
   548  		[]flow.Identifier{conID.NodeID}, []flow.Identifier{conID.NodeID})
   549  	require.NoError(t, err)
   550  	block.Header.ParentVoterIndices = voterIndices
   551  	block.Header.View = 42
   552  	block.SetPayload(flow.Payload{})
   553  	proposal := unittest.ProposalFromBlock(&block)
   554  
   555  	child := unittest.BlockWithParentAndProposerFixture(block.Header, conID.NodeID, 1)
   556  	child.Header.ParentVoterIndices = voterIndices
   557  
   558  	actualCalls := atomic.Uint64{}
   559  
   560  	verificationEngine := new(mocknetwork.Engine)
   561  	_, _ = verification1Node.Net.Register(channels.ReceiveReceipts, verificationEngine)
   562  	_, _ = verification2Node.Net.Register(channels.ReceiveReceipts, verificationEngine)
   563  	verificationEngine.On("Process", mock.AnythingOfType("channels.Channel"), exeID.NodeID, mock.Anything).
   564  		Run(func(args mock.Arguments) {
   565  			actualCalls.Inc()
   566  
   567  			var receipt *flow.ExecutionReceipt
   568  			receipt, _ = args[2].(*flow.ExecutionReceipt)
   569  
   570  			assert.Equal(t, block.ID(), receipt.ExecutionResult.BlockID)
   571  		}).
   572  		Return(nil)
   573  
   574  	err = sendBlock(&exeNode, exeID.NodeID, proposal)
   575  	require.NoError(t, err)
   576  
   577  	err = sendBlock(&exeNode, conID.NodeID, unittest.ProposalFromBlock(&child))
   578  	require.NoError(t, err)
   579  
   580  	hub.DeliverAllEventually(t, func() bool {
   581  		return actualCalls.Load() == 2
   582  	})
   583  
   584  	verificationEngine.AssertExpectations(t)
   585  }
   586  
   587  // Test that when received the same state delta for the second time,
   588  // the delta will be saved again without causing any error.
   589  // func TestReceiveTheSameDeltaMultipleTimes(t *testing.T) {
   590  // 	hub := stub.NewNetworkHub()
   591  //
   592  // 	chainID := flow.Mainnet
   593  //
   594  // 	colID := unittest.IdentityFixture(unittest.WithRole(flow.RoleCollection))
   595  // 	exeID := unittest.IdentityFixture(unittest.WithRole(flow.RoleExecution))
   596  // 	ver1ID := unittest.IdentityFixture(unittest.WithRole(flow.RoleVerification))
   597  // 	ver2ID := unittest.IdentityFixture(unittest.WithRole(flow.RoleVerification))
   598  //
   599  // 	identities := unittest.CompleteIdentitySet(colID, exeID, ver1ID, ver2ID)
   600  //
   601  // 	exeNode := testutil.ExecutionNode(t, hub, exeID, identities, 21, chainID)
   602  // 	defer exeNode.Done()
   603  //
   604  // 	genesis, err := exeNode.State.AtHeight(0).Head()
   605  // 	require.NoError(t, err)
   606  //
   607  // 	delta := unittest.StateDeltaWithParentFixture(genesis)
   608  // 	delta.ExecutableBlock.StartState = unittest.GenesisStateCommitment
   609  // 	delta.EndState = unittest.GenesisStateCommitment
   610  //
   611  // 	fmt.Printf("block id: %v, delta for block (%v)'s parent id: %v\n", genesis.ID(), delta.Block.ID(), delta.ParentID())
   612  // 	exeNode.IngestionEngine.SubmitLocal(delta)
   613  // 	time.Sleep(time.Second)
   614  //
   615  // 	exeNode.IngestionEngine.SubmitLocal(delta)
   616  // 	// handling the same delta again to verify the DB calls in saveExecutionResults
   617  // 	// are idempotent, if they weren't, it will hit log.Fatal and crash before
   618  // 	// sleep is done
   619  // 	time.Sleep(time.Second)
   620  //
   621  // }