github.com/MetalBlockchain/subnet-evm@v0.4.9/plugin/evm/syncervm_client.go (about)

     1  // (c) 2021-2022, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package evm
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"sync"
    10  
    11  	"github.com/MetalBlockchain/metalgo/database"
    12  	"github.com/MetalBlockchain/metalgo/database/versiondb"
    13  	"github.com/MetalBlockchain/metalgo/ids"
    14  	"github.com/MetalBlockchain/metalgo/snow/choices"
    15  	commonEng "github.com/MetalBlockchain/metalgo/snow/engine/common"
    16  	"github.com/MetalBlockchain/metalgo/snow/engine/snowman/block"
    17  	"github.com/MetalBlockchain/metalgo/vms/components/chain"
    18  	"github.com/MetalBlockchain/subnet-evm/core/rawdb"
    19  	"github.com/MetalBlockchain/subnet-evm/core/state/snapshot"
    20  	"github.com/MetalBlockchain/subnet-evm/eth"
    21  	"github.com/MetalBlockchain/subnet-evm/ethdb"
    22  	"github.com/MetalBlockchain/subnet-evm/params"
    23  	"github.com/MetalBlockchain/subnet-evm/plugin/evm/message"
    24  	syncclient "github.com/MetalBlockchain/subnet-evm/sync/client"
    25  	"github.com/MetalBlockchain/subnet-evm/sync/statesync"
    26  	"github.com/ethereum/go-ethereum/common"
    27  	"github.com/ethereum/go-ethereum/log"
    28  )
    29  
    30  const (
    31  	// State sync fetches [parentsToGet] parents of the block it syncs to.
    32  	// The last 256 block hashes are necessary to support the BLOCKHASH opcode.
    33  	parentsToGet = 256
    34  )
    35  
    36  var stateSyncSummaryKey = []byte("stateSyncSummary")
    37  
    38  // stateSyncClientConfig defines the options and dependencies needed to construct a StateSyncerClient
    39  type stateSyncClientConfig struct {
    40  	enabled    bool
    41  	skipResume bool
    42  	// Specifies the number of blocks behind the latest state summary that the chain must be
    43  	// in order to prefer performing state sync over falling back to the normal bootstrapping
    44  	// algorithm.
    45  	stateSyncMinBlocks uint64
    46  
    47  	lastAcceptedHeight uint64
    48  
    49  	chain           *eth.Ethereum
    50  	state           *chain.State
    51  	chaindb         ethdb.Database
    52  	metadataDB      database.Database
    53  	acceptedBlockDB database.Database
    54  	db              *versiondb.Database
    55  
    56  	client syncclient.Client
    57  
    58  	toEngine chan<- commonEng.Message
    59  }
    60  
    61  type stateSyncerClient struct {
    62  	*stateSyncClientConfig
    63  
    64  	resumableSummary message.SyncSummary
    65  
    66  	cancel context.CancelFunc
    67  	wg     sync.WaitGroup
    68  
    69  	// State Sync results
    70  	syncSummary  message.SyncSummary
    71  	stateSyncErr error
    72  }
    73  
    74  func NewStateSyncClient(config *stateSyncClientConfig) StateSyncClient {
    75  	return &stateSyncerClient{
    76  		stateSyncClientConfig: config,
    77  	}
    78  }
    79  
    80  type StateSyncClient interface {
    81  	// methods that implement the client side of [block.StateSyncableVM]
    82  	StateSyncEnabled(context.Context) (bool, error)
    83  	GetOngoingSyncStateSummary(context.Context) (block.StateSummary, error)
    84  	ParseStateSummary(ctx context.Context, summaryBytes []byte) (block.StateSummary, error)
    85  
    86  	// additional methods required by the evm package
    87  	StateSyncClearOngoingSummary() error
    88  	Shutdown() error
    89  	Error() error
    90  }
    91  
    92  // Syncer represents a step in state sync,
    93  // along with Start/Done methods to control
    94  // and monitor progress.
    95  // Error returns an error if any was encountered.
    96  type Syncer interface {
    97  	Start(ctx context.Context) error
    98  	Done() <-chan error
    99  }
   100  
   101  // StateSyncEnabled returns [client.enabled], which is set in the chain's config file.
   102  func (client *stateSyncerClient) StateSyncEnabled(context.Context) (bool, error) {
   103  	return client.enabled, nil
   104  }
   105  
   106  // GetOngoingSyncStateSummary returns a state summary that was previously started
   107  // and not finished, and sets [resumableSummary] if one was found.
   108  // Returns [database.ErrNotFound] if no ongoing summary is found or if [client.skipResume] is true.
   109  func (client *stateSyncerClient) GetOngoingSyncStateSummary(context.Context) (block.StateSummary, error) {
   110  	if client.skipResume {
   111  		return nil, database.ErrNotFound
   112  	}
   113  
   114  	summaryBytes, err := client.metadataDB.Get(stateSyncSummaryKey)
   115  	if err != nil {
   116  		return nil, err // includes the [database.ErrNotFound] case
   117  	}
   118  
   119  	summary, err := message.NewSyncSummaryFromBytes(summaryBytes, client.acceptSyncSummary)
   120  	if err != nil {
   121  		return nil, fmt.Errorf("failed to parse saved state sync summary to SyncSummary: %w", err)
   122  	}
   123  	client.resumableSummary = summary
   124  	return summary, nil
   125  }
   126  
   127  // StateSyncClearOngoingSummary clears any marker of an ongoing state sync summary
   128  func (client *stateSyncerClient) StateSyncClearOngoingSummary() error {
   129  	if err := client.metadataDB.Delete(stateSyncSummaryKey); err != nil {
   130  		return fmt.Errorf("failed to clear ongoing summary: %w", err)
   131  	}
   132  	if err := client.db.Commit(); err != nil {
   133  		return fmt.Errorf("failed to commit db while clearing ongoing summary: %w", err)
   134  	}
   135  
   136  	return nil
   137  }
   138  
   139  // ParseStateSummary parses [summaryBytes] to [commonEng.Summary]
   140  func (client *stateSyncerClient) ParseStateSummary(_ context.Context, summaryBytes []byte) (block.StateSummary, error) {
   141  	return message.NewSyncSummaryFromBytes(summaryBytes, client.acceptSyncSummary)
   142  }
   143  
   144  // stateSync blockingly performs the state sync for the EVM state and the atomic state
   145  // to [client.syncSummary]. returns an error if one occurred.
   146  func (client *stateSyncerClient) stateSync(ctx context.Context) error {
   147  	if err := client.syncBlocks(ctx, client.syncSummary.BlockHash, client.syncSummary.BlockNumber, parentsToGet); err != nil {
   148  		return err
   149  	}
   150  
   151  	// Sync the EVM trie.
   152  	return client.syncStateTrie(ctx)
   153  }
   154  
   155  // acceptSyncSummary returns true if sync will be performed and launches the state sync process
   156  // in a goroutine.
   157  func (client *stateSyncerClient) acceptSyncSummary(proposedSummary message.SyncSummary) (block.StateSyncMode, error) {
   158  	isResume := proposedSummary.BlockHash == client.resumableSummary.BlockHash
   159  	if !isResume {
   160  		// Skip syncing if the blockchain is not significantly ahead of local state,
   161  		// since bootstrapping would be faster.
   162  		// (Also ensures we don't sync to a height prior to local state.)
   163  		if client.lastAcceptedHeight+client.stateSyncMinBlocks > proposedSummary.Height() {
   164  			log.Info(
   165  				"last accepted too close to most recent syncable block, skipping state sync",
   166  				"lastAccepted", client.lastAcceptedHeight,
   167  				"syncableHeight", proposedSummary.Height(),
   168  			)
   169  			if err := client.StateSyncClearOngoingSummary(); err != nil {
   170  				return block.StateSyncSkipped, fmt.Errorf("failed to clear ongoing summary after skipping state sync: %w", err)
   171  			}
   172  			// Initialize snapshots if we're skipping state sync, since it will not have been initialized on
   173  			// startup.
   174  			client.chain.BlockChain().InitializeSnapshots()
   175  			return block.StateSyncSkipped, nil
   176  		}
   177  
   178  		// Wipe the snapshot completely if we are not resuming from an existing sync, so that we do not
   179  		// use a corrupted snapshot.
   180  		// Note: this assumes that when the node is started with state sync disabled, the in-progress state
   181  		// sync marker will be wiped, so we do not accidentally resume progress from an incorrect version
   182  		// of the snapshot. (if switching between versions that come before this change and back this could
   183  		// lead to the snapshot not being cleaned up correctly)
   184  		<-snapshot.WipeSnapshot(client.chaindb, true)
   185  		// Reset the snapshot generator here so that when state sync completes, snapshots will not attempt to read an
   186  		// invalid generator.
   187  		// Note: this must be called after WipeSnapshot is called so that we do not invalidate a partially generated snapshot.
   188  		snapshot.ResetSnapshotGeneration(client.chaindb)
   189  	}
   190  	client.syncSummary = proposedSummary
   191  
   192  	// Update the current state sync summary key in the database
   193  	// Note: this must be performed after WipeSnapshot finishes so that we do not start a state sync
   194  	// session from a partially wiped snapshot.
   195  	if err := client.metadataDB.Put(stateSyncSummaryKey, proposedSummary.Bytes()); err != nil {
   196  		return block.StateSyncSkipped, fmt.Errorf("failed to write state sync summary key to disk: %w", err)
   197  	}
   198  	if err := client.db.Commit(); err != nil {
   199  		return block.StateSyncSkipped, fmt.Errorf("failed to commit db: %w", err)
   200  	}
   201  
   202  	log.Info("Starting state sync", "summary", proposedSummary)
   203  
   204  	// create a cancellable ctx for the state sync goroutine
   205  	ctx, cancel := context.WithCancel(context.Background())
   206  	client.cancel = cancel
   207  	client.wg.Add(1) // track the state sync goroutine so we can wait for it on shutdown
   208  	go func() {
   209  		defer client.wg.Done()
   210  		defer cancel()
   211  
   212  		if err := client.stateSync(ctx); err != nil {
   213  			client.stateSyncErr = err
   214  		} else {
   215  			client.stateSyncErr = client.finishSync()
   216  		}
   217  		// notify engine regardless of whether err == nil,
   218  		// this error will be propagated to the engine when it calls
   219  		// vm.SetState(snow.Bootstrapping)
   220  		log.Info("stateSync completed, notifying engine", "err", client.stateSyncErr)
   221  		client.toEngine <- commonEng.StateSyncDone
   222  	}()
   223  	return block.StateSyncStatic, nil
   224  }
   225  
   226  // syncBlocks fetches (up to) [parentsToGet] blocks from peers
   227  // using [client] and writes them to disk.
   228  // the process begins with [fromHash] and it fetches parents recursively.
   229  // fetching starts from the first ancestor not found on disk
   230  func (client *stateSyncerClient) syncBlocks(ctx context.Context, fromHash common.Hash, fromHeight uint64, parentsToGet int) error {
   231  	nextHash := fromHash
   232  	nextHeight := fromHeight
   233  	parentsPerRequest := uint16(32)
   234  
   235  	// first, check for blocks already available on disk so we don't
   236  	// request them from peers.
   237  	for parentsToGet >= 0 {
   238  		blk := rawdb.ReadBlock(client.chaindb, nextHash, nextHeight)
   239  		if blk != nil {
   240  			// block exists
   241  			nextHash = blk.ParentHash()
   242  			nextHeight--
   243  			parentsToGet--
   244  			continue
   245  		}
   246  
   247  		// block was not found
   248  		break
   249  	}
   250  
   251  	// get any blocks we couldn't find on disk from peers and write
   252  	// them to disk.
   253  	batch := client.chaindb.NewBatch()
   254  	for i := parentsToGet - 1; i >= 0 && (nextHash != common.Hash{}); {
   255  		if err := ctx.Err(); err != nil {
   256  			return err
   257  		}
   258  		blocks, err := client.client.GetBlocks(ctx, nextHash, nextHeight, parentsPerRequest)
   259  		if err != nil {
   260  			log.Warn("could not get blocks from peer", "err", err, "nextHash", nextHash, "remaining", i+1)
   261  			return err
   262  		}
   263  		for _, block := range blocks {
   264  			rawdb.WriteBlock(batch, block)
   265  			rawdb.WriteCanonicalHash(batch, block.Hash(), block.NumberU64())
   266  
   267  			i--
   268  			nextHash = block.ParentHash()
   269  			nextHeight--
   270  		}
   271  		log.Info("fetching blocks from peer", "remaining", i+1, "total", parentsToGet)
   272  	}
   273  	log.Info("fetched blocks from peer", "total", parentsToGet)
   274  	return batch.Write()
   275  }
   276  
   277  func (client *stateSyncerClient) syncStateTrie(ctx context.Context) error {
   278  	log.Info("state sync: sync starting", "root", client.syncSummary.BlockRoot)
   279  	evmSyncer, err := statesync.NewStateSyncer(&statesync.StateSyncerConfig{
   280  		Client:                   client.client,
   281  		Root:                     client.syncSummary.BlockRoot,
   282  		BatchSize:                ethdb.IdealBatchSize,
   283  		DB:                       client.chaindb,
   284  		MaxOutstandingCodeHashes: statesync.DefaultMaxOutstandingCodeHashes,
   285  		NumCodeFetchingWorkers:   statesync.DefaultNumCodeFetchingWorkers,
   286  	})
   287  	if err != nil {
   288  		return err
   289  	}
   290  	if err := evmSyncer.Start(ctx); err != nil {
   291  		return err
   292  	}
   293  	err = <-evmSyncer.Done()
   294  	log.Info("state sync: sync finished", "root", client.syncSummary.BlockRoot, "err", err)
   295  	return err
   296  }
   297  
   298  func (client *stateSyncerClient) Shutdown() error {
   299  	if client.cancel != nil {
   300  		client.cancel()
   301  	}
   302  	client.wg.Wait() // wait for the background goroutine to exit
   303  	return nil
   304  }
   305  
   306  // finishSync is responsible for updating disk and memory pointers so the VM is prepared
   307  // for bootstrapping. Executes any shared memory operations from the atomic trie to shared memory.
   308  func (client *stateSyncerClient) finishSync() error {
   309  	stateBlock, err := client.state.GetBlock(context.TODO(), ids.ID(client.syncSummary.BlockHash))
   310  	if err != nil {
   311  		return fmt.Errorf("could not get block by hash from client state: %s", client.syncSummary.BlockHash)
   312  	}
   313  
   314  	wrapper, ok := stateBlock.(*chain.BlockWrapper)
   315  	if !ok {
   316  		return fmt.Errorf("could not convert block(%T) to *chain.BlockWrapper", wrapper)
   317  	}
   318  	evmBlock, ok := wrapper.Block.(*Block)
   319  	if !ok {
   320  		return fmt.Errorf("could not convert block(%T) to evm.Block", stateBlock)
   321  	}
   322  
   323  	evmBlock.SetStatus(choices.Accepted)
   324  	block := evmBlock.ethBlock
   325  
   326  	if block.Hash() != client.syncSummary.BlockHash {
   327  		return fmt.Errorf("attempted to set last summary block to unexpected block hash: (%s != %s)", block.Hash(), client.syncSummary.BlockHash)
   328  	}
   329  	if block.NumberU64() != client.syncSummary.BlockNumber {
   330  		return fmt.Errorf("attempted to set last summary block to unexpected block number: (%d != %d)", block.NumberU64(), client.syncSummary.BlockNumber)
   331  	}
   332  
   333  	// BloomIndexer needs to know that some parts of the chain are not available
   334  	// and cannot be indexed. This is done by calling [AddCheckpoint] here.
   335  	// Since the indexer uses sections of size [params.BloomBitsBlocks] (= 4096),
   336  	// each block is indexed in section number [blockNumber/params.BloomBitsBlocks].
   337  	// To allow the indexer to start with the block we just synced to,
   338  	// we create a checkpoint for its parent.
   339  	// Note: This requires assuming the synced block height is divisible
   340  	// by [params.BloomBitsBlocks].
   341  	parentHeight := block.NumberU64() - 1
   342  	parentHash := block.ParentHash()
   343  	client.chain.BloomIndexer().AddCheckpoint(parentHeight/params.BloomBitsBlocks, parentHash)
   344  
   345  	if err := client.chain.BlockChain().ResetToStateSyncedBlock(block); err != nil {
   346  		return err
   347  	}
   348  
   349  	if err := client.updateVMMarkers(); err != nil {
   350  		return fmt.Errorf("error updating vm markers, height=%d, hash=%s, err=%w", block.NumberU64(), block.Hash(), err)
   351  	}
   352  
   353  	return client.state.SetLastAcceptedBlock(evmBlock)
   354  }
   355  
   356  // updateVMMarkers updates the following markers in the VM's database
   357  // and commits them atomically:
   358  // - updates atomic trie so it will have necessary metadata for the last committed root
   359  // - updates atomic trie so it will resume applying operations to shared memory on initialize
   360  // - updates lastAcceptedKey
   361  // - removes state sync progress markers
   362  func (client *stateSyncerClient) updateVMMarkers() error {
   363  	if err := client.acceptedBlockDB.Put(lastAcceptedKey, client.syncSummary.BlockHash[:]); err != nil {
   364  		return err
   365  	}
   366  	if err := client.metadataDB.Delete(stateSyncSummaryKey); err != nil {
   367  		return err
   368  	}
   369  	return client.db.Commit()
   370  }
   371  
   372  // Error returns a non-nil error if one occurred during the sync.
   373  func (client *stateSyncerClient) Error() error { return client.stateSyncErr }