github.com/dim4egster/coreth@v0.10.2/sync/statesync/code_syncer.go (about) 1 // (c) 2021-2022, Ava Labs, Inc. All rights reserved. 2 // See the file LICENSE for licensing terms. 3 4 package statesync 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "sync" 11 12 "github.com/dim4egster/qmallgo/ids" 13 "github.com/dim4egster/coreth/core/rawdb" 14 "github.com/dim4egster/coreth/ethdb" 15 "github.com/dim4egster/coreth/params" 16 statesyncclient "github.com/dim4egster/coreth/sync/client" 17 "github.com/ethereum/go-ethereum/common" 18 ) 19 20 const ( 21 DefaultMaxOutstandingCodeHashes = 5000 22 DefaultNumCodeFetchingWorkers = 5 23 ) 24 25 var errFailedToAddCodeHashesToQueue = errors.New("failed to add code hashes to queue") 26 27 // CodeSyncerConfig defines the configuration of the code syncer 28 type CodeSyncerConfig struct { 29 // Maximum number of outstanding code hashes in the queue before the code syncer should block. 30 MaxOutstandingCodeHashes int 31 // Number of worker threads to fetch code from the network 32 NumCodeFetchingWorkers int 33 34 // Client for fetching code from the network 35 Client statesyncclient.Client 36 37 // Database for the code syncer to use. 38 DB ethdb.Database 39 } 40 41 // codeSyncer syncs code bytes from the network in a seprate thread. 42 // Tracks outstanding requests in the DB, so that it will still fulfill them if interrupted. 43 type codeSyncer struct { 44 lock sync.Mutex 45 46 CodeSyncerConfig 47 48 outstandingCodeHashes ids.Set // Set of code hashes that we need to fetch from the network. 49 codeHashes chan common.Hash // Channel of incoming code hash requests 50 51 // Used to set terminal error or pass nil to [errChan] if successful. 52 errOnce sync.Once 53 errChan chan error 54 55 // Passed in details from the context used to start the codeSyncer 56 cancel context.CancelFunc 57 done <-chan struct{} 58 } 59 60 // newCodeSyncer returns a a code syncer that will sync code bytes from the network in a separate thread. 61 func newCodeSyncer(config CodeSyncerConfig) *codeSyncer { 62 return &codeSyncer{ 63 CodeSyncerConfig: config, 64 codeHashes: make(chan common.Hash, config.MaxOutstandingCodeHashes), 65 outstandingCodeHashes: ids.NewSet(0), 66 errChan: make(chan error, 1), 67 } 68 } 69 70 // start the worker thread and populate the code hashes queue with active work. 71 // blocks until all outstanding code requests from a previous sync have been 72 // queued for fetching (or ctx is cancelled). 73 func (c *codeSyncer) start(ctx context.Context) { 74 ctx, c.cancel = context.WithCancel(ctx) 75 c.done = ctx.Done() 76 wg := sync.WaitGroup{} 77 78 // Start [numCodeFetchingWorkers] threads to fetch code from the network. 79 for i := 0; i < c.NumCodeFetchingWorkers; i++ { 80 wg.Add(1) 81 go func() { 82 defer wg.Done() 83 84 if err := c.work(ctx); err != nil { 85 c.setError(err) 86 } 87 }() 88 } 89 90 err := c.addCodeToFetchFromDBToQueue() 91 if err != nil { 92 c.setError(err) 93 } 94 95 // Wait for all the worker threads to complete before signalling success via setError(nil). 96 // Note: if any of the worker threads errored already, setError will be a no-op here. 97 go func() { 98 wg.Wait() 99 c.setError(nil) 100 }() 101 } 102 103 // Clean out any codeToFetch markers from the database that are no longer needed and 104 // add any outstanding markers to the queue. 105 func (c *codeSyncer) addCodeToFetchFromDBToQueue() error { 106 it := rawdb.NewCodeToFetchIterator(c.DB) 107 defer it.Release() 108 109 batch := c.DB.NewBatch() 110 codeHashes := make([]common.Hash, 0) 111 for it.Next() { 112 codeHash := common.BytesToHash(it.Key()[len(rawdb.CodeToFetchPrefix):]) 113 // If we already have the codeHash, delete the marker from the database and continue 114 if rawdb.HasCode(c.DB, codeHash) { 115 rawdb.DeleteCodeToFetch(batch, codeHash) 116 // Write the batch to disk if it has reached the ideal batch size. 117 if batch.ValueSize() > ethdb.IdealBatchSize { 118 if err := batch.Write(); err != nil { 119 return fmt.Errorf("failed to write batch removing old code markers: %w", err) 120 } 121 batch.Reset() 122 } 123 continue 124 } 125 126 codeHashes = append(codeHashes, codeHash) 127 } 128 if err := it.Error(); err != nil { 129 return fmt.Errorf("failed to iterate code entries to fetch: %w", err) 130 } 131 if batch.ValueSize() > 0 { 132 if err := batch.Write(); err != nil { 133 return fmt.Errorf("failed to write batch removing old code markers: %w", err) 134 } 135 } 136 return c.addCode(codeHashes) 137 } 138 139 // work fulfills any incoming requests from the producer channel by fetching code bytes from the network 140 // and fulfilling them by updating the database. 141 func (c *codeSyncer) work(ctx context.Context) error { 142 codeHashes := make([]common.Hash, 0, params.MaxCodeHashesPerRequest) 143 144 for { 145 select { 146 case <-ctx.Done(): // If ctx is done, set the error to the ctx error since work has been cancelled. 147 return ctx.Err() 148 case codeHash, ok := <-c.codeHashes: 149 // If there are no more [codeHashes], fulfill a last code request for any [codeHashes] previously 150 // read from the channel, then return. 151 if !ok { 152 if len(codeHashes) > 0 { 153 return c.fulfillCodeRequest(ctx, codeHashes) 154 } 155 return nil 156 } 157 158 codeHashes = append(codeHashes, codeHash) 159 // Try to wait for at least [MaxCodeHashesPerRequest] code hashes to batch into a single request 160 // if there's more work remaining. 161 if len(codeHashes) < params.MaxCodeHashesPerRequest { 162 continue 163 } 164 if err := c.fulfillCodeRequest(ctx, codeHashes); err != nil { 165 return err 166 } 167 168 // Reset the codeHashes array 169 codeHashes = codeHashes[:0] 170 } 171 } 172 } 173 174 // fulfillCodeRequest sends a request for [codeHashes], writes the result to the database, and 175 // marks the work as complete. 176 // codeHashes should not be empty or contain duplicate hashes. 177 // Returns an error if one is encountered, signaling the worker thread to terminate. 178 func (c *codeSyncer) fulfillCodeRequest(ctx context.Context, codeHashes []common.Hash) error { 179 codeByteSlices, err := c.Client.GetCode(ctx, codeHashes) 180 if err != nil { 181 return err 182 } 183 184 // Hold the lock while modifying outstandingCodeHashes. 185 c.lock.Lock() 186 batch := c.DB.NewBatch() 187 for i, codeHash := range codeHashes { 188 rawdb.DeleteCodeToFetch(batch, codeHash) 189 c.outstandingCodeHashes.Remove(ids.ID(codeHash)) 190 rawdb.WriteCode(batch, codeHash, codeByteSlices[i]) 191 } 192 c.lock.Unlock() // Release the lock before writing the batch 193 194 if err := batch.Write(); err != nil { 195 return fmt.Errorf("faild to write batch for fulfilled code requests: %w", err) 196 } 197 return nil 198 } 199 200 // addCode checks if [codeHashes] need to be fetched from the network and adds them to the queue if so. 201 // assumes that [codeHashes] are valid non-empty code hashes. 202 func (c *codeSyncer) addCode(codeHashes []common.Hash) error { 203 batch := c.DB.NewBatch() 204 205 c.lock.Lock() 206 selectedCodeHashes := make([]common.Hash, 0, len(codeHashes)) 207 for _, codeHash := range codeHashes { 208 // Add the code hash to the queue if it's not already on the queue and we do not already have it 209 // in the database. 210 if !c.outstandingCodeHashes.Contains(ids.ID(codeHash)) && !rawdb.HasCode(c.DB, codeHash) { 211 selectedCodeHashes = append(selectedCodeHashes, codeHash) 212 c.outstandingCodeHashes.Add(ids.ID(codeHash)) 213 rawdb.AddCodeToFetch(batch, codeHash) 214 } 215 } 216 c.lock.Unlock() 217 218 if err := batch.Write(); err != nil { 219 return fmt.Errorf("failed to write batch of code to fetch markers due to: %w", err) 220 } 221 return c.addHashesToQueue(selectedCodeHashes) 222 } 223 224 // notifyAccountTrieCompleted notifies the code syncer that there will be no more incoming 225 // code hashes from syncing the account trie, so it only needs to compelete its outstanding 226 // work. 227 // Note: this allows the worker threads to exit and return a nil error. 228 func (c *codeSyncer) notifyAccountTrieCompleted() { 229 close(c.codeHashes) 230 } 231 232 // addHashesToQueue adds [codeHashes] to the queue and blocks until it is able to do so. 233 // This should be called after all other operation to add code hashes to the queue has been completed. 234 func (c *codeSyncer) addHashesToQueue(codeHashes []common.Hash) error { 235 for _, codeHash := range codeHashes { 236 select { 237 case c.codeHashes <- codeHash: 238 case <-c.done: 239 return errFailedToAddCodeHashesToQueue 240 } 241 } 242 return nil 243 } 244 245 // setError sets the error to the first error that occurs and adds it to the error channel. 246 // If [err] is nil, setError indicates that codeSyncer has finished code syncing successfully. 247 func (c *codeSyncer) setError(err error) { 248 c.errOnce.Do(func() { 249 c.cancel() 250 c.errChan <- err 251 }) 252 } 253 254 // Done returns an error channel to indicate the return status of code syncing. 255 func (c *codeSyncer) Done() <-chan error { return c.errChan }