github.com/koko1123/flow-go-1@v0.29.6/module/finalizer/collection/finalizer_test.go (about)

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