github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/finalizer/collection/finalizer_test.go (about)

     1  package collection_test
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/dgraph-io/badger/v2"
     7  	"github.com/stretchr/testify/assert"
     8  	"github.com/stretchr/testify/mock"
     9  	"github.com/stretchr/testify/require"
    10  
    11  	model "github.com/onflow/flow-go/model/cluster"
    12  	"github.com/onflow/flow-go/model/flow"
    13  	"github.com/onflow/flow-go/model/messages"
    14  	"github.com/onflow/flow-go/module/finalizer/collection"
    15  	"github.com/onflow/flow-go/module/mempool/herocache"
    16  	"github.com/onflow/flow-go/module/metrics"
    17  	"github.com/onflow/flow-go/network/mocknetwork"
    18  	cluster "github.com/onflow/flow-go/state/cluster/badger"
    19  	"github.com/onflow/flow-go/storage/badger/operation"
    20  	"github.com/onflow/flow-go/storage/badger/procedure"
    21  	"github.com/onflow/flow-go/utils/unittest"
    22  )
    23  
    24  func TestFinalizer(t *testing.T) {
    25  	unittest.RunWithBadgerDB(t, func(db *badger.DB) {
    26  		// reference block on the main consensus chain
    27  		refBlock := unittest.BlockHeaderFixture()
    28  		// genesis block for the cluster chain
    29  		genesis := model.Genesis()
    30  
    31  		metrics := metrics.NewNoopCollector()
    32  
    33  		var state *cluster.State
    34  
    35  		pool := herocache.NewTransactions(1000, unittest.Logger(), metrics)
    36  
    37  		// a helper function to clean up shared state between tests
    38  		cleanup := func() {
    39  			// wipe the DB
    40  			err := db.DropAll()
    41  			require.Nil(t, err)
    42  			// clear the mempool
    43  			for _, tx := range pool.All() {
    44  				pool.Remove(tx.ID())
    45  			}
    46  		}
    47  
    48  		// a helper function to bootstrap with the genesis block
    49  		bootstrap := func() {
    50  			stateRoot, err := cluster.NewStateRoot(genesis, unittest.QuorumCertificateFixture(), 0)
    51  			require.NoError(t, err)
    52  			state, err = cluster.Bootstrap(db, stateRoot)
    53  			require.NoError(t, err)
    54  			err = db.Update(operation.InsertHeader(refBlock.ID(), refBlock))
    55  			require.NoError(t, err)
    56  		}
    57  
    58  		// a helper function to insert a block
    59  		insert := func(block model.Block) {
    60  			err := db.Update(procedure.InsertClusterBlock(&block))
    61  			assert.Nil(t, err)
    62  		}
    63  
    64  		t.Run("non-existent block", func(t *testing.T) {
    65  			bootstrap()
    66  			defer cleanup()
    67  
    68  			prov := new(mocknetwork.Engine)
    69  			prov.On("SubmitLocal", mock.Anything)
    70  			finalizer := collection.NewFinalizer(db, pool, prov, metrics)
    71  
    72  			fakeBlockID := unittest.IdentifierFixture()
    73  			err := finalizer.MakeFinal(fakeBlockID)
    74  			assert.Error(t, err)
    75  		})
    76  
    77  		t.Run("already finalized block", func(t *testing.T) {
    78  			bootstrap()
    79  			defer cleanup()
    80  
    81  			prov := new(mocknetwork.Engine)
    82  			prov.On("SubmitLocal", mock.Anything)
    83  			finalizer := collection.NewFinalizer(db, pool, prov, metrics)
    84  
    85  			// tx1 is included in the finalized block
    86  			tx1 := unittest.TransactionBodyFixture(func(tx *flow.TransactionBody) { tx.ProposalKey.SequenceNumber = 1 })
    87  			assert.True(t, pool.Add(&tx1))
    88  
    89  			// create a new block on genesis
    90  			block := unittest.ClusterBlockWithParent(genesis)
    91  			block.SetPayload(model.PayloadFromTransactions(refBlock.ID(), &tx1))
    92  			insert(block)
    93  
    94  			// finalize the block
    95  			err := finalizer.MakeFinal(block.ID())
    96  			assert.Nil(t, err)
    97  
    98  			// finalize the block again - this should be a no-op
    99  			err = finalizer.MakeFinal(block.ID())
   100  			assert.Nil(t, err)
   101  		})
   102  
   103  		t.Run("unconnected block", func(t *testing.T) {
   104  			bootstrap()
   105  			defer cleanup()
   106  
   107  			prov := new(mocknetwork.Engine)
   108  			prov.On("SubmitLocal", mock.Anything)
   109  			finalizer := collection.NewFinalizer(db, pool, prov, metrics)
   110  
   111  			// create a new block that isn't connected to a parent
   112  			block := unittest.ClusterBlockWithParent(genesis)
   113  			block.Header.ParentID = unittest.IdentifierFixture()
   114  			block.SetPayload(model.EmptyPayload(refBlock.ID()))
   115  			insert(block)
   116  
   117  			// try to finalize - this should fail
   118  			err := finalizer.MakeFinal(block.ID())
   119  			assert.Error(t, err)
   120  		})
   121  
   122  		t.Run("empty collection block", func(t *testing.T) {
   123  			bootstrap()
   124  			defer cleanup()
   125  
   126  			prov := new(mocknetwork.Engine)
   127  			finalizer := collection.NewFinalizer(db, pool, prov, metrics)
   128  
   129  			// create a block with empty payload on genesis
   130  			block := unittest.ClusterBlockWithParent(genesis)
   131  			block.SetPayload(model.EmptyPayload(refBlock.ID()))
   132  			insert(block)
   133  
   134  			// finalize the block
   135  			err := finalizer.MakeFinal(block.ID())
   136  			assert.Nil(t, err)
   137  
   138  			// check finalized boundary using cluster state
   139  			final, err := state.Final().Head()
   140  			assert.Nil(t, err)
   141  			assert.Equal(t, block.ID(), final.ID())
   142  
   143  			// collection should not have been propagated
   144  			prov.AssertNotCalled(t, "SubmitLocal", mock.Anything)
   145  		})
   146  
   147  		t.Run("finalize single block", func(t *testing.T) {
   148  			bootstrap()
   149  			defer cleanup()
   150  
   151  			prov := new(mocknetwork.Engine)
   152  			prov.On("SubmitLocal", mock.Anything)
   153  			finalizer := collection.NewFinalizer(db, pool, prov, metrics)
   154  
   155  			// tx1 is included in the finalized block and mempool
   156  			tx1 := unittest.TransactionBodyFixture(func(tx *flow.TransactionBody) { tx.ProposalKey.SequenceNumber = 1 })
   157  			assert.True(t, pool.Add(&tx1))
   158  			// tx2 is only in the mempool
   159  			tx2 := unittest.TransactionBodyFixture(func(tx *flow.TransactionBody) { tx.ProposalKey.SequenceNumber = 2 })
   160  			assert.True(t, pool.Add(&tx2))
   161  
   162  			// create a block containing tx1 on top of genesis
   163  			block := unittest.ClusterBlockWithParent(genesis)
   164  			block.SetPayload(model.PayloadFromTransactions(refBlock.ID(), &tx1))
   165  			insert(block)
   166  
   167  			// finalize the block
   168  			err := finalizer.MakeFinal(block.ID())
   169  			assert.Nil(t, err)
   170  
   171  			// tx1 should have been removed from mempool
   172  			assert.False(t, pool.Has(tx1.ID()))
   173  			// tx2 should still be in mempool
   174  			assert.True(t, pool.Has(tx2.ID()))
   175  
   176  			// check finalized boundary using cluster state
   177  			final, err := state.Final().Head()
   178  			assert.Nil(t, err)
   179  			assert.Equal(t, block.ID(), final.ID())
   180  			assertClusterBlocksIndexedByReferenceHeight(t, db, refBlock.Height, final.ID())
   181  
   182  			// block should be passed to provider
   183  			prov.AssertNumberOfCalls(t, "SubmitLocal", 1)
   184  			prov.AssertCalled(t, "SubmitLocal", &messages.SubmitCollectionGuarantee{
   185  				Guarantee: flow.CollectionGuarantee{
   186  					CollectionID:     block.Payload.Collection.ID(),
   187  					ReferenceBlockID: refBlock.ID(),
   188  					ChainID:          block.Header.ChainID,
   189  					SignerIndices:    block.Header.ParentVoterIndices,
   190  					Signature:        nil,
   191  				},
   192  			})
   193  		})
   194  
   195  		// when finalizing a block with un-finalized ancestors, those ancestors should be finalized as well
   196  		t.Run("finalize multiple blocks together", func(t *testing.T) {
   197  			bootstrap()
   198  			defer cleanup()
   199  
   200  			prov := new(mocknetwork.Engine)
   201  			prov.On("SubmitLocal", mock.Anything)
   202  			finalizer := collection.NewFinalizer(db, pool, prov, metrics)
   203  
   204  			// tx1 is included in the first finalized block and mempool
   205  			tx1 := unittest.TransactionBodyFixture(func(tx *flow.TransactionBody) { tx.ProposalKey.SequenceNumber = 1 })
   206  			assert.True(t, pool.Add(&tx1))
   207  			// tx2 is included in the second finalized block and mempool
   208  			tx2 := unittest.TransactionBodyFixture(func(tx *flow.TransactionBody) { tx.ProposalKey.SequenceNumber = 2 })
   209  			assert.True(t, pool.Add(&tx2))
   210  
   211  			// create a block containing tx1 on top of genesis
   212  			block1 := unittest.ClusterBlockWithParent(genesis)
   213  			block1.SetPayload(model.PayloadFromTransactions(refBlock.ID(), &tx1))
   214  			insert(block1)
   215  
   216  			// create a block containing tx2 on top of block1
   217  			block2 := unittest.ClusterBlockWithParent(&block1)
   218  			block2.SetPayload(model.PayloadFromTransactions(refBlock.ID(), &tx2))
   219  			insert(block2)
   220  
   221  			// finalize block2 (should indirectly finalize block1 as well)
   222  			err := finalizer.MakeFinal(block2.ID())
   223  			assert.Nil(t, err)
   224  
   225  			// tx1 and tx2 should have been removed from mempool
   226  			assert.False(t, pool.Has(tx1.ID()))
   227  			assert.False(t, pool.Has(tx2.ID()))
   228  
   229  			// check finalized boundary using cluster state
   230  			final, err := state.Final().Head()
   231  			assert.Nil(t, err)
   232  			assert.Equal(t, block2.ID(), final.ID())
   233  			assertClusterBlocksIndexedByReferenceHeight(t, db, refBlock.Height, block1.ID(), block2.ID())
   234  
   235  			// both blocks should be passed to provider
   236  			prov.AssertNumberOfCalls(t, "SubmitLocal", 2)
   237  			prov.AssertCalled(t, "SubmitLocal", &messages.SubmitCollectionGuarantee{
   238  				Guarantee: flow.CollectionGuarantee{
   239  					CollectionID:     block1.Payload.Collection.ID(),
   240  					ReferenceBlockID: refBlock.ID(),
   241  					ChainID:          block1.Header.ChainID,
   242  					SignerIndices:    block1.Header.ParentVoterIndices,
   243  					Signature:        nil,
   244  				},
   245  			})
   246  			prov.AssertCalled(t, "SubmitLocal", &messages.SubmitCollectionGuarantee{
   247  				Guarantee: flow.CollectionGuarantee{
   248  					CollectionID:     block2.Payload.Collection.ID(),
   249  					ReferenceBlockID: refBlock.ID(),
   250  					ChainID:          block2.Header.ChainID,
   251  					SignerIndices:    block2.Header.ParentVoterIndices,
   252  					Signature:        nil,
   253  				},
   254  			})
   255  		})
   256  
   257  		t.Run("finalize with un-finalized child", func(t *testing.T) {
   258  			bootstrap()
   259  			defer cleanup()
   260  
   261  			prov := new(mocknetwork.Engine)
   262  			prov.On("SubmitLocal", mock.Anything)
   263  			finalizer := collection.NewFinalizer(db, pool, prov, metrics)
   264  
   265  			// tx1 is included in the finalized parent block and mempool
   266  			tx1 := unittest.TransactionBodyFixture(func(tx *flow.TransactionBody) { tx.ProposalKey.SequenceNumber = 1 })
   267  			assert.True(t, pool.Add(&tx1))
   268  			// tx2 is included in the un-finalized block and mempool
   269  			tx2 := unittest.TransactionBodyFixture(func(tx *flow.TransactionBody) { tx.ProposalKey.SequenceNumber = 2 })
   270  			assert.True(t, pool.Add(&tx2))
   271  
   272  			// create a block containing tx1 on top of genesis
   273  			block1 := unittest.ClusterBlockWithParent(genesis)
   274  			block1.SetPayload(model.PayloadFromTransactions(refBlock.ID(), &tx1))
   275  			insert(block1)
   276  
   277  			// create a block containing tx2 on top of block1
   278  			block2 := unittest.ClusterBlockWithParent(&block1)
   279  			block2.SetPayload(model.PayloadFromTransactions(refBlock.ID(), &tx2))
   280  			insert(block2)
   281  
   282  			// finalize block1 (should NOT finalize block2)
   283  			err := finalizer.MakeFinal(block1.ID())
   284  			assert.Nil(t, err)
   285  
   286  			// tx1 should have been removed from mempool
   287  			assert.False(t, pool.Has(tx1.ID()))
   288  			// tx2 should NOT have been removed from mempool (since block2 wasn't finalized)
   289  			assert.True(t, pool.Has(tx2.ID()))
   290  
   291  			// check finalized boundary using cluster state
   292  			final, err := state.Final().Head()
   293  			assert.Nil(t, err)
   294  			assert.Equal(t, block1.ID(), final.ID())
   295  			assertClusterBlocksIndexedByReferenceHeight(t, db, refBlock.Height, block1.ID())
   296  
   297  			// block should be passed to provider
   298  			prov.AssertNumberOfCalls(t, "SubmitLocal", 1)
   299  			prov.AssertCalled(t, "SubmitLocal", &messages.SubmitCollectionGuarantee{
   300  				Guarantee: flow.CollectionGuarantee{
   301  					CollectionID:     block1.Payload.Collection.ID(),
   302  					ReferenceBlockID: refBlock.ID(),
   303  					ChainID:          block1.Header.ChainID,
   304  					SignerIndices:    block1.Header.ParentVoterIndices,
   305  					Signature:        nil,
   306  				},
   307  			})
   308  		})
   309  
   310  		// when finalizing a block with a conflicting fork, the fork should not be finalized.
   311  		t.Run("conflicting fork", func(t *testing.T) {
   312  			bootstrap()
   313  			defer cleanup()
   314  
   315  			prov := new(mocknetwork.Engine)
   316  			prov.On("SubmitLocal", mock.Anything)
   317  			finalizer := collection.NewFinalizer(db, pool, prov, metrics)
   318  
   319  			// tx1 is included in the finalized block and mempool
   320  			tx1 := unittest.TransactionBodyFixture(func(tx *flow.TransactionBody) { tx.ProposalKey.SequenceNumber = 1 })
   321  			assert.True(t, pool.Add(&tx1))
   322  			// tx2 is included in the conflicting block and mempool
   323  			tx2 := unittest.TransactionBodyFixture(func(tx *flow.TransactionBody) { tx.ProposalKey.SequenceNumber = 2 })
   324  			assert.True(t, pool.Add(&tx2))
   325  
   326  			// create a block containing tx1 on top of genesis
   327  			block1 := unittest.ClusterBlockWithParent(genesis)
   328  			block1.SetPayload(model.PayloadFromTransactions(refBlock.ID(), &tx1))
   329  			insert(block1)
   330  
   331  			// create a block containing tx2 on top of genesis (conflicting with block1)
   332  			block2 := unittest.ClusterBlockWithParent(genesis)
   333  			block2.SetPayload(model.PayloadFromTransactions(refBlock.ID(), &tx2))
   334  			insert(block2)
   335  
   336  			// finalize block1
   337  			err := finalizer.MakeFinal(block1.ID())
   338  			assert.Nil(t, err)
   339  
   340  			// tx1 should have been removed from mempool
   341  			assert.False(t, pool.Has(tx1.ID()))
   342  			// tx2 should NOT have been removed from mempool (since block2 wasn't finalized)
   343  			assert.True(t, pool.Has(tx2.ID()))
   344  
   345  			// check finalized boundary using cluster state
   346  			final, err := state.Final().Head()
   347  			assert.Nil(t, err)
   348  			assert.Equal(t, block1.ID(), final.ID())
   349  			assertClusterBlocksIndexedByReferenceHeight(t, db, refBlock.Height, block1.ID())
   350  
   351  			// block should be passed to provider
   352  			prov.AssertNumberOfCalls(t, "SubmitLocal", 1)
   353  			prov.AssertCalled(t, "SubmitLocal", &messages.SubmitCollectionGuarantee{
   354  				Guarantee: flow.CollectionGuarantee{
   355  					CollectionID:     block1.Payload.Collection.ID(),
   356  					ReferenceBlockID: refBlock.ID(),
   357  					ChainID:          block1.Header.ChainID,
   358  					SignerIndices:    block1.Header.ParentVoterIndices,
   359  					Signature:        nil,
   360  				},
   361  			})
   362  		})
   363  	})
   364  }
   365  
   366  // assertClusterBlocksIndexedByReferenceHeight checks the given cluster blocks have
   367  // been indexed by the given reference block height, which is expected as part of
   368  // finalization.
   369  func assertClusterBlocksIndexedByReferenceHeight(t *testing.T, db *badger.DB, refHeight uint64, clusterBlockIDs ...flow.Identifier) {
   370  	var ids []flow.Identifier
   371  	err := db.View(operation.LookupClusterBlocksByReferenceHeightRange(refHeight, refHeight, &ids))
   372  	require.NoError(t, err)
   373  	assert.ElementsMatch(t, clusterBlockIDs, ids)
   374  }