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 }