github.com/koko1123/flow-go-1@v0.29.6/module/chainsync/core_rapid_test.go (about) 1 package chainsync 2 3 import ( 4 "io" 5 "testing" 6 7 "github.com/rs/zerolog" 8 "github.com/stretchr/testify/assert" 9 "github.com/stretchr/testify/require" 10 11 "pgregory.net/rapid" 12 13 "github.com/koko1123/flow-go-1/model/flow" 14 "github.com/koko1123/flow-go-1/module/metrics" 15 "github.com/koko1123/flow-go-1/utils/unittest" 16 ) 17 18 // TODO: remove this restriction and write a proper parametrizable generator (à la SliceOfN) once we have 19 // a FlatMap combinator in rapid 20 const NUM_BLOCKS int = 100 21 22 // This returns a forest of blocks, some of which are in a parent relationship 23 // It should include forks 24 func populatedBlockStore(t *rapid.T) []*flow.Header { 25 store := []*flow.Header{unittest.BlockHeaderFixture()} 26 for i := 1; i < NUM_BLOCKS; i++ { 27 // we sample from the store 2/3 times to get deeper trees 28 b := rapid.OneOf(rapid.Just(unittest.BlockHeaderFixture()), rapid.SampledFrom(store), rapid.SampledFrom(store)).Draw(t, "parent").(flow.Header) 29 store = append(store, unittest.BlockHeaderWithParentFixture(&b)) 30 } 31 return store 32 } 33 34 type rapidSync struct { 35 store []*flow.Header 36 core *Core 37 idRequests map[flow.Identifier]bool // depth 1 pushdown automaton to track ID requests 38 heightRequests map[uint64]bool // depth 1 pushdown automaton to track height requests 39 } 40 41 // Init is an action for initializing a rapidSync instance. 42 func (r *rapidSync) Init(t *rapid.T) { 43 var err error 44 45 r.core, err = New(zerolog.New(io.Discard), DefaultConfig(), metrics.NewNoopCollector()) 46 require.NoError(t, err) 47 48 r.store = populatedBlockStore(t) 49 r.idRequests = make(map[flow.Identifier]bool) 50 r.heightRequests = make(map[uint64]bool) 51 } 52 53 // RequestByID is an action that requests a block by its ID. 54 func (r *rapidSync) RequestByID(t *rapid.T) { 55 b := rapid.SampledFrom(r.store).Draw(t, "id_request").(*flow.Header) 56 r.core.RequestBlock(b.ID(), b.Height) 57 // Re-queueing by ID should always succeed 58 r.idRequests[b.ID()] = true 59 // Re-qeueuing by ID "forgets" a past height request 60 r.heightRequests[b.Height] = false 61 } 62 63 // RequestByHeight is an action that requests a specific height 64 func (r *rapidSync) RequestByHeight(t *rapid.T) { 65 b := rapid.SampledFrom(r.store).Draw(t, "height_request").(*flow.Header) 66 r.core.RequestHeight(b.Height) 67 // Re-queueing by height should always succeed 68 r.heightRequests[b.Height] = true 69 } 70 71 // HandleHeight is an action that requests a heights 72 // upon receiving an argument beyond a certain tolerance 73 func (r *rapidSync) HandleHeight(t *rapid.T) { 74 b := rapid.SampledFrom(r.store).Draw(t, "height_hint_request").(*flow.Header) 75 incr := rapid.IntRange(0, (int)(DefaultConfig().Tolerance)+1).Draw(t, "height increment").(int) 76 requestHeight := b.Height + (uint64)(incr) 77 r.core.HandleHeight(b, requestHeight) 78 // Re-queueing by height should always succeed if beyond tolerance 79 if (uint)(incr) > DefaultConfig().Tolerance { 80 for h := b.Height + 1; h <= requestHeight; h++ { 81 r.heightRequests[h] = true 82 } 83 } 84 } 85 86 // HandleByID is an action that provides a block header to the sync engine 87 func (r *rapidSync) HandleByID(t *rapid.T) { 88 b := rapid.SampledFrom(r.store).Draw(t, "id_handling").(*flow.Header) 89 success := r.core.HandleBlock(b) 90 assert.True(t, success || r.idRequests[b.ID()] == false) 91 92 // we decrease the pending requests iff we have already requested this block 93 // and we have not received it since 94 if r.idRequests[b.ID()] == true { 95 r.idRequests[b.ID()] = false 96 } 97 // we eagerly remove height requests for blocks we receive 98 r.heightRequests[b.Height] = false 99 } 100 101 // Check runs after every action and verifies that all required invariants hold. 102 func (r *rapidSync) Check(t *rapid.T) { 103 // we collect the received blocks as determined above 104 var receivedBlocks []*flow.Header 105 // we also collect the pending blocks 106 var activeBlocks []*flow.Header 107 108 // we check the validity of our pushdown automaton for ID requests and populate activeBlocks / receivedBlocks 109 for id, requested := range r.idRequests { 110 s, foundID := r.core.blockIDs[id] 111 112 block, foundBlock := findHeader(r.store, func(h *flow.Header) bool { 113 return h.ID() == id 114 }) 115 require.True(t, foundBlock, "incorrect management of idRequests in the tests: all added IDs are supposed to be from the store") 116 117 if requested { 118 require.True(t, foundID, "ID %v is supposed to be known, but isn't", id) 119 120 assert.True(t, s.WasQueued(), "ID %v was expected to be Queued and is %v", id, s.StatusString()) 121 assert.False(t, s.WasReceived(), "ID %v was expected to be Queued and is %v", id, s.StatusString()) 122 activeBlocks = append(activeBlocks, block) 123 } else { 124 if foundID { 125 // if a block is known with 0 pendings, it's because it was received 126 assert.True(t, s.WasReceived(), "ID %v was expected to be Received and is %v", id, s.StatusString()) 127 receivedBlocks = append(receivedBlocks, block) 128 } 129 } 130 } 131 132 // we collect still-active heights 133 var activeHeights []uint64 134 135 for h, requested := range r.heightRequests { 136 s, ok := r.core.heights[h] 137 if requested { 138 require.True(t, ok, "Height %x is supposed to be known, but isn't", h) 139 assert.True(t, s.WasQueued(), "Height %x was expected to be Queued and is %v", h, s.StatusString()) 140 assert.False(t, s.WasReceived(), "Height %x was expected to be Queued and is %v", h, s.StatusString()) 141 activeHeights = append(activeHeights, h) 142 143 } else { 144 // if a height is known with 0 pendings, it's because: 145 // - it was received 146 // - or because a block at this height was (blockAtHeightWasReceived) 147 // - or because a request for a block at that height made us "forget" the prior height reception (clobberedByID) 148 if ok { 149 wasReceived := s.WasReceived() 150 _, blockAtHeightWasReceived := findHeader(receivedBlocks, func(header *flow.Header) bool { 151 return header.Height == h 152 }) 153 _, clobberedByID := findHeader(activeBlocks, func(header *flow.Header) bool { 154 return header.Height == h 155 }) 156 heightWasCanceled := wasReceived || blockAtHeightWasReceived || clobberedByID 157 158 assert.True(t, heightWasCanceled, "Height %x was expected to be Received (or filled through a same-height block) and is %v", h, s.StatusString()) 159 } 160 } 161 } 162 163 heights, blockIDs := r.core.getRequestableItems() 164 // the queueing logic queues intervals, while our r.heightRequests only queues specific requests 165 assert.Subset(t, heights, activeHeights, "sync engine's height request tracking lost heights") 166 167 for _, bID := range blockIDs { 168 v, ok := r.idRequests[bID] 169 require.True(t, ok) 170 assert.Equal(t, true, v, "blockID %v is supposed to be pending but is not", bID) 171 } 172 } 173 174 func TestRapidSync(t *testing.T) { 175 unittest.SkipUnless(t, unittest.TEST_FLAKY, "flaky test") 176 177 rapid.Check(t, rapid.Run(&rapidSync{})) 178 } 179 180 // utility functions 181 func findHeader(store []*flow.Header, predicate func(*flow.Header) bool) (*flow.Header, bool) { 182 for _, b := range store { 183 if predicate(b) { 184 return b, true 185 } 186 } 187 return nil, false 188 }