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 }