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 }