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 }