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 }