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