github.com/calmw/ethereum@v0.1.1/eth/handler.go (about) 1 // Copyright 2015 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 package eth 18 19 import ( 20 "errors" 21 "math" 22 "math/big" 23 "sync" 24 "sync/atomic" 25 "time" 26 27 "github.com/calmw/ethereum/common" 28 "github.com/calmw/ethereum/consensus" 29 "github.com/calmw/ethereum/consensus/beacon" 30 "github.com/calmw/ethereum/core" 31 "github.com/calmw/ethereum/core/forkid" 32 "github.com/calmw/ethereum/core/types" 33 "github.com/calmw/ethereum/eth/downloader" 34 "github.com/calmw/ethereum/eth/fetcher" 35 "github.com/calmw/ethereum/eth/protocols/eth" 36 "github.com/calmw/ethereum/eth/protocols/snap" 37 "github.com/calmw/ethereum/ethdb" 38 "github.com/calmw/ethereum/event" 39 "github.com/calmw/ethereum/log" 40 "github.com/calmw/ethereum/p2p" 41 ) 42 43 const ( 44 // txChanSize is the size of channel listening to NewTxsEvent. 45 // The number is referenced from the size of tx pool. 46 txChanSize = 4096 47 ) 48 49 var ( 50 syncChallengeTimeout = 15 * time.Second // Time allowance for a node to reply to the sync progress challenge 51 ) 52 53 // txPool defines the methods needed from a transaction pool implementation to 54 // support all the operations needed by the Ethereum chain protocols. 55 type txPool interface { 56 // Has returns an indicator whether txpool has a transaction 57 // cached with the given hash. 58 Has(hash common.Hash) bool 59 60 // Get retrieves the transaction from local txpool with given 61 // tx hash. 62 Get(hash common.Hash) *types.Transaction 63 64 // AddRemotes should add the given transactions to the pool. 65 AddRemotes([]*types.Transaction) []error 66 67 // Pending should return pending transactions. 68 // The slice should be modifiable by the caller. 69 Pending(enforceTips bool) map[common.Address]types.Transactions 70 71 // SubscribeNewTxsEvent should return an event subscription of 72 // NewTxsEvent and send events to the given channel. 73 SubscribeNewTxsEvent(chan<- core.NewTxsEvent) event.Subscription 74 } 75 76 // handlerConfig is the collection of initialization parameters to create a full 77 // node network handler. 78 type handlerConfig struct { 79 Database ethdb.Database // Database for direct sync insertions 80 Chain *core.BlockChain // Blockchain to serve data from 81 TxPool txPool // Transaction pool to propagate from 82 Merger *consensus.Merger // The manager for eth1/2 transition 83 Network uint64 // Network identifier to adfvertise 84 Sync downloader.SyncMode // Whether to snap or full sync 85 BloomCache uint64 // Megabytes to alloc for snap sync bloom 86 EventMux *event.TypeMux // Legacy event mux, deprecate for `feed` 87 RequiredBlocks map[uint64]common.Hash // Hard coded map of required block hashes for sync challenges 88 } 89 90 type handler struct { 91 networkID uint64 92 forkFilter forkid.Filter // Fork ID filter, constant across the lifetime of the node 93 94 snapSync atomic.Bool // Flag whether snap sync is enabled (gets disabled if we already have blocks) 95 acceptTxs atomic.Bool // Flag whether we're considered synchronised (enables transaction processing) 96 97 database ethdb.Database 98 txpool txPool 99 chain *core.BlockChain 100 maxPeers int 101 102 downloader *downloader.Downloader 103 blockFetcher *fetcher.BlockFetcher 104 txFetcher *fetcher.TxFetcher 105 peers *peerSet 106 merger *consensus.Merger 107 108 eventMux *event.TypeMux 109 txsCh chan core.NewTxsEvent 110 txsSub event.Subscription 111 minedBlockSub *event.TypeMuxSubscription 112 113 requiredBlocks map[uint64]common.Hash 114 115 // channels for fetcher, syncer, txsyncLoop 116 quitSync chan struct{} 117 118 chainSync *chainSyncer 119 wg sync.WaitGroup 120 peerWG sync.WaitGroup 121 } 122 123 // newHandler returns a handler for all Ethereum chain management protocol. 124 func newHandler(config *handlerConfig) (*handler, error) { 125 // Create the protocol manager with the base fields 126 if config.EventMux == nil { 127 config.EventMux = new(event.TypeMux) // Nicety initialization for tests 128 } 129 h := &handler{ 130 networkID: config.Network, 131 forkFilter: forkid.NewFilter(config.Chain), 132 eventMux: config.EventMux, 133 database: config.Database, 134 txpool: config.TxPool, 135 chain: config.Chain, 136 peers: newPeerSet(), 137 merger: config.Merger, 138 requiredBlocks: config.RequiredBlocks, 139 quitSync: make(chan struct{}), 140 } 141 if config.Sync == downloader.FullSync { 142 // The database seems empty as the current block is the genesis. Yet the snap 143 // block is ahead, so snap sync was enabled for this node at a certain point. 144 // The scenarios where this can happen is 145 // * if the user manually (or via a bad block) rolled back a snap sync node 146 // below the sync point. 147 // * the last snap sync is not finished while user specifies a full sync this 148 // time. But we don't have any recent state for full sync. 149 // In these cases however it's safe to reenable snap sync. 150 fullBlock, snapBlock := h.chain.CurrentBlock(), h.chain.CurrentSnapBlock() 151 if fullBlock.Number.Uint64() == 0 && snapBlock.Number.Uint64() > 0 { 152 h.snapSync.Store(true) 153 log.Warn("Switch sync mode from full sync to snap sync") 154 } 155 } else { 156 if h.chain.CurrentBlock().Number.Uint64() > 0 { 157 // Print warning log if database is not empty to run snap sync. 158 log.Warn("Switch sync mode from snap sync to full sync") 159 } else { 160 // If snap sync was requested and our database is empty, grant it 161 h.snapSync.Store(true) 162 } 163 } 164 // If sync succeeds, pass a callback to potentially disable snap sync mode 165 // and enable transaction propagation. 166 success := func() { 167 // If we were running snap sync and it finished, disable doing another 168 // round on next sync cycle 169 if h.snapSync.Load() { 170 log.Info("Snap sync complete, auto disabling") 171 h.snapSync.Store(false) 172 } 173 // If we've successfully finished a sync cycle, accept transactions from 174 // the network 175 h.acceptTxs.Store(true) 176 } 177 // Construct the downloader (long sync) 178 h.downloader = downloader.New(config.Database, h.eventMux, h.chain, nil, h.removePeer, success) 179 if ttd := h.chain.Config().TerminalTotalDifficulty; ttd != nil { 180 if h.chain.Config().TerminalTotalDifficultyPassed { 181 log.Info("Chain post-merge, sync via beacon client") 182 } else { 183 head := h.chain.CurrentBlock() 184 if td := h.chain.GetTd(head.Hash(), head.Number.Uint64()); td.Cmp(ttd) >= 0 { 185 log.Info("Chain post-TTD, sync via beacon client") 186 } else { 187 log.Warn("Chain pre-merge, sync via PoW (ensure beacon client is ready)") 188 } 189 } 190 } else if h.chain.Config().TerminalTotalDifficultyPassed { 191 log.Error("Chain configured post-merge, but without TTD. Are you debugging sync?") 192 } 193 // Construct the fetcher (short sync) 194 validator := func(header *types.Header) error { 195 // All the block fetcher activities should be disabled 196 // after the transition. Print the warning log. 197 if h.merger.PoSFinalized() { 198 log.Warn("Unexpected validation activity", "hash", header.Hash(), "number", header.Number) 199 return errors.New("unexpected behavior after transition") 200 } 201 // Reject all the PoS style headers in the first place. No matter 202 // the chain has finished the transition or not, the PoS headers 203 // should only come from the trusted consensus layer instead of 204 // p2p network. 205 if beacon, ok := h.chain.Engine().(*beacon.Beacon); ok { 206 if beacon.IsPoSHeader(header) { 207 return errors.New("unexpected post-merge header") 208 } 209 } 210 return h.chain.Engine().VerifyHeader(h.chain, header) 211 } 212 heighter := func() uint64 { 213 return h.chain.CurrentBlock().Number.Uint64() 214 } 215 inserter := func(blocks types.Blocks) (int, error) { 216 // All the block fetcher activities should be disabled 217 // after the transition. Print the warning log. 218 if h.merger.PoSFinalized() { 219 var ctx []interface{} 220 ctx = append(ctx, "blocks", len(blocks)) 221 if len(blocks) > 0 { 222 ctx = append(ctx, "firsthash", blocks[0].Hash()) 223 ctx = append(ctx, "firstnumber", blocks[0].Number()) 224 ctx = append(ctx, "lasthash", blocks[len(blocks)-1].Hash()) 225 ctx = append(ctx, "lastnumber", blocks[len(blocks)-1].Number()) 226 } 227 log.Warn("Unexpected insertion activity", ctx...) 228 return 0, errors.New("unexpected behavior after transition") 229 } 230 // If snap sync is running, deny importing weird blocks. This is a problematic 231 // clause when starting up a new network, because snap-syncing miners might not 232 // accept each others' blocks until a restart. Unfortunately we haven't figured 233 // out a way yet where nodes can decide unilaterally whether the network is new 234 // or not. This should be fixed if we figure out a solution. 235 if h.snapSync.Load() { 236 log.Warn("Snap syncing, discarded propagated block", "number", blocks[0].Number(), "hash", blocks[0].Hash()) 237 return 0, nil 238 } 239 if h.merger.TDDReached() { 240 // The blocks from the p2p network is regarded as untrusted 241 // after the transition. In theory block gossip should be disabled 242 // entirely whenever the transition is started. But in order to 243 // handle the transition boundary reorg in the consensus-layer, 244 // the legacy blocks are still accepted, but only for the terminal 245 // pow blocks. Spec: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-3675.md#halt-the-importing-of-pow-blocks 246 for i, block := range blocks { 247 ptd := h.chain.GetTd(block.ParentHash(), block.NumberU64()-1) 248 if ptd == nil { 249 return 0, nil 250 } 251 td := new(big.Int).Add(ptd, block.Difficulty()) 252 if !h.chain.Config().IsTerminalPoWBlock(ptd, td) { 253 log.Info("Filtered out non-termimal pow block", "number", block.NumberU64(), "hash", block.Hash()) 254 return 0, nil 255 } 256 if err := h.chain.InsertBlockWithoutSetHead(block); err != nil { 257 return i, err 258 } 259 } 260 return 0, nil 261 } 262 n, err := h.chain.InsertChain(blocks) 263 if err == nil { 264 h.acceptTxs.Store(true) // Mark initial sync done on any fetcher import 265 } 266 return n, err 267 } 268 h.blockFetcher = fetcher.NewBlockFetcher(false, nil, h.chain.GetBlockByHash, validator, h.BroadcastBlock, heighter, nil, inserter, h.removePeer) 269 270 fetchTx := func(peer string, hashes []common.Hash) error { 271 p := h.peers.peer(peer) 272 if p == nil { 273 return errors.New("unknown peer") 274 } 275 return p.RequestTxs(hashes) 276 } 277 h.txFetcher = fetcher.NewTxFetcher(h.txpool.Has, h.txpool.AddRemotes, fetchTx) 278 h.chainSync = newChainSyncer(h) 279 return h, nil 280 } 281 282 // runEthPeer registers an eth peer into the joint eth/snap peerset, adds it to 283 // various subsystems and starts handling messages. 284 func (h *handler) runEthPeer(peer *eth.Peer, handler eth.Handler) error { 285 // If the peer has a `snap` extension, wait for it to connect so we can have 286 // a uniform initialization/teardown mechanism 287 snap, err := h.peers.waitSnapExtension(peer) 288 if err != nil { 289 peer.Log().Error("Snapshot extension barrier failed", "err", err) 290 return err 291 } 292 // TODO(karalabe): Not sure why this is needed 293 if !h.chainSync.handlePeerEvent(peer) { 294 return p2p.DiscQuitting 295 } 296 h.peerWG.Add(1) 297 defer h.peerWG.Done() 298 299 // Execute the Ethereum handshake 300 var ( 301 genesis = h.chain.Genesis() 302 head = h.chain.CurrentHeader() 303 hash = head.Hash() 304 number = head.Number.Uint64() 305 td = h.chain.GetTd(hash, number) 306 ) 307 forkID := forkid.NewID(h.chain.Config(), genesis.Hash(), number, head.Time) 308 if err := peer.Handshake(h.networkID, td, hash, genesis.Hash(), forkID, h.forkFilter); err != nil { 309 peer.Log().Debug("Ethereum handshake failed", "err", err) 310 return err 311 } 312 reject := false // reserved peer slots 313 if h.snapSync.Load() { 314 if snap == nil { 315 // If we are running snap-sync, we want to reserve roughly half the peer 316 // slots for peers supporting the snap protocol. 317 // The logic here is; we only allow up to 5 more non-snap peers than snap-peers. 318 if all, snp := h.peers.len(), h.peers.snapLen(); all-snp > snp+5 { 319 reject = true 320 } 321 } 322 } 323 // Ignore maxPeers if this is a trusted peer 324 if !peer.Peer.Info().Network.Trusted { 325 if reject || h.peers.len() >= h.maxPeers { 326 return p2p.DiscTooManyPeers 327 } 328 } 329 peer.Log().Debug("Ethereum peer connected", "name", peer.Name()) 330 331 // Register the peer locally 332 if err := h.peers.registerPeer(peer, snap); err != nil { 333 peer.Log().Error("Ethereum peer registration failed", "err", err) 334 return err 335 } 336 defer h.unregisterPeer(peer.ID()) 337 338 p := h.peers.peer(peer.ID()) 339 if p == nil { 340 return errors.New("peer dropped during handling") 341 } 342 // Register the peer in the downloader. If the downloader considers it banned, we disconnect 343 if err := h.downloader.RegisterPeer(peer.ID(), peer.Version(), peer); err != nil { 344 peer.Log().Error("Failed to register peer in eth syncer", "err", err) 345 return err 346 } 347 if snap != nil { 348 if err := h.downloader.SnapSyncer.Register(snap); err != nil { 349 peer.Log().Error("Failed to register peer in snap syncer", "err", err) 350 return err 351 } 352 } 353 h.chainSync.handlePeerEvent(peer) 354 355 // Propagate existing transactions. new transactions appearing 356 // after this will be sent via broadcasts. 357 h.syncTransactions(peer) 358 359 // Create a notification channel for pending requests if the peer goes down 360 dead := make(chan struct{}) 361 defer close(dead) 362 363 // If we have any explicit peer required block hashes, request them 364 for number, hash := range h.requiredBlocks { 365 resCh := make(chan *eth.Response) 366 367 req, err := peer.RequestHeadersByNumber(number, 1, 0, false, resCh) 368 if err != nil { 369 return err 370 } 371 go func(number uint64, hash common.Hash, req *eth.Request) { 372 // Ensure the request gets cancelled in case of error/drop 373 defer req.Close() 374 375 timeout := time.NewTimer(syncChallengeTimeout) 376 defer timeout.Stop() 377 378 select { 379 case res := <-resCh: 380 headers := ([]*types.Header)(*res.Res.(*eth.BlockHeadersPacket)) 381 if len(headers) == 0 { 382 // Required blocks are allowed to be missing if the remote 383 // node is not yet synced 384 res.Done <- nil 385 return 386 } 387 // Validate the header and either drop the peer or continue 388 if len(headers) > 1 { 389 res.Done <- errors.New("too many headers in required block response") 390 return 391 } 392 if headers[0].Number.Uint64() != number || headers[0].Hash() != hash { 393 peer.Log().Info("Required block mismatch, dropping peer", "number", number, "hash", headers[0].Hash(), "want", hash) 394 res.Done <- errors.New("required block mismatch") 395 return 396 } 397 peer.Log().Debug("Peer required block verified", "number", number, "hash", hash) 398 res.Done <- nil 399 case <-timeout.C: 400 peer.Log().Warn("Required block challenge timed out, dropping", "addr", peer.RemoteAddr(), "type", peer.Name()) 401 h.removePeer(peer.ID()) 402 } 403 }(number, hash, req) 404 } 405 // Handle incoming messages until the connection is torn down 406 return handler(peer) 407 } 408 409 // runSnapExtension registers a `snap` peer into the joint eth/snap peerset and 410 // starts handling inbound messages. As `snap` is only a satellite protocol to 411 // `eth`, all subsystem registrations and lifecycle management will be done by 412 // the main `eth` handler to prevent strange races. 413 func (h *handler) runSnapExtension(peer *snap.Peer, handler snap.Handler) error { 414 h.peerWG.Add(1) 415 defer h.peerWG.Done() 416 417 if err := h.peers.registerSnapExtension(peer); err != nil { 418 peer.Log().Warn("Snapshot extension registration failed", "err", err) 419 return err 420 } 421 return handler(peer) 422 } 423 424 // removePeer requests disconnection of a peer. 425 func (h *handler) removePeer(id string) { 426 peer := h.peers.peer(id) 427 if peer != nil { 428 peer.Peer.Disconnect(p2p.DiscUselessPeer) 429 } 430 } 431 432 // unregisterPeer removes a peer from the downloader, fetchers and main peer set. 433 func (h *handler) unregisterPeer(id string) { 434 // Create a custom logger to avoid printing the entire id 435 var logger log.Logger 436 if len(id) < 16 { 437 // Tests use short IDs, don't choke on them 438 logger = log.New("peer", id) 439 } else { 440 logger = log.New("peer", id[:8]) 441 } 442 // Abort if the peer does not exist 443 peer := h.peers.peer(id) 444 if peer == nil { 445 logger.Error("Ethereum peer removal failed", "err", errPeerNotRegistered) 446 return 447 } 448 // Remove the `eth` peer if it exists 449 logger.Debug("Removing Ethereum peer", "snap", peer.snapExt != nil) 450 451 // Remove the `snap` extension if it exists 452 if peer.snapExt != nil { 453 h.downloader.SnapSyncer.Unregister(id) 454 } 455 h.downloader.UnregisterPeer(id) 456 h.txFetcher.Drop(id) 457 458 if err := h.peers.unregisterPeer(id); err != nil { 459 logger.Error("Ethereum peer removal failed", "err", err) 460 } 461 } 462 463 func (h *handler) Start(maxPeers int) { 464 h.maxPeers = maxPeers 465 466 // broadcast transactions 467 h.wg.Add(1) 468 h.txsCh = make(chan core.NewTxsEvent, txChanSize) 469 h.txsSub = h.txpool.SubscribeNewTxsEvent(h.txsCh) 470 go h.txBroadcastLoop() 471 472 // broadcast mined blocks 473 h.wg.Add(1) 474 h.minedBlockSub = h.eventMux.Subscribe(core.NewMinedBlockEvent{}) 475 go h.minedBroadcastLoop() 476 477 // start sync handlers 478 h.wg.Add(1) 479 go h.chainSync.loop() 480 } 481 482 func (h *handler) Stop() { 483 h.txsSub.Unsubscribe() // quits txBroadcastLoop 484 h.minedBlockSub.Unsubscribe() // quits blockBroadcastLoop 485 486 // Quit chainSync and txsync64. 487 // After this is done, no new peers will be accepted. 488 close(h.quitSync) 489 h.wg.Wait() 490 491 // Disconnect existing sessions. 492 // This also closes the gate for any new registrations on the peer set. 493 // sessions which are already established but not added to h.peers yet 494 // will exit when they try to register. 495 h.peers.close() 496 h.peerWG.Wait() 497 498 log.Info("Ethereum protocol stopped") 499 } 500 501 // BroadcastBlock will either propagate a block to a subset of its peers, or 502 // will only announce its availability (depending what's requested). 503 func (h *handler) BroadcastBlock(block *types.Block, propagate bool) { 504 // Disable the block propagation if the chain has already entered the PoS 505 // stage. The block propagation is delegated to the consensus layer. 506 if h.merger.PoSFinalized() { 507 return 508 } 509 // Disable the block propagation if it's the post-merge block. 510 if beacon, ok := h.chain.Engine().(*beacon.Beacon); ok { 511 if beacon.IsPoSHeader(block.Header()) { 512 return 513 } 514 } 515 hash := block.Hash() 516 peers := h.peers.peersWithoutBlock(hash) 517 518 // If propagation is requested, send to a subset of the peer 519 if propagate { 520 // Calculate the TD of the block (it's not imported yet, so block.Td is not valid) 521 var td *big.Int 522 if parent := h.chain.GetBlock(block.ParentHash(), block.NumberU64()-1); parent != nil { 523 td = new(big.Int).Add(block.Difficulty(), h.chain.GetTd(block.ParentHash(), block.NumberU64()-1)) 524 } else { 525 log.Error("Propagating dangling block", "number", block.Number(), "hash", hash) 526 return 527 } 528 // Send the block to a subset of our peers 529 transfer := peers[:int(math.Sqrt(float64(len(peers))))] 530 for _, peer := range transfer { 531 peer.AsyncSendNewBlock(block, td) 532 } 533 log.Trace("Propagated block", "hash", hash, "recipients", len(transfer), "duration", common.PrettyDuration(time.Since(block.ReceivedAt))) 534 return 535 } 536 // Otherwise if the block is indeed in out own chain, announce it 537 if h.chain.HasBlock(hash, block.NumberU64()) { 538 for _, peer := range peers { 539 peer.AsyncSendNewBlockHash(block) 540 } 541 log.Trace("Announced block", "hash", hash, "recipients", len(peers), "duration", common.PrettyDuration(time.Since(block.ReceivedAt))) 542 } 543 } 544 545 // BroadcastTransactions will propagate a batch of transactions 546 // - To a square root of all peers 547 // - And, separately, as announcements to all peers which are not known to 548 // already have the given transaction. 549 func (h *handler) BroadcastTransactions(txs types.Transactions) { 550 var ( 551 annoCount int // Count of announcements made 552 annoPeers int 553 directCount int // Count of the txs sent directly to peers 554 directPeers int // Count of the peers that were sent transactions directly 555 556 txset = make(map[*ethPeer][]common.Hash) // Set peer->hash to transfer directly 557 annos = make(map[*ethPeer][]common.Hash) // Set peer->hash to announce 558 559 ) 560 // Broadcast transactions to a batch of peers not knowing about it 561 for _, tx := range txs { 562 peers := h.peers.peersWithoutTransaction(tx.Hash()) 563 // Send the tx unconditionally to a subset of our peers 564 numDirect := int(math.Sqrt(float64(len(peers)))) 565 for _, peer := range peers[:numDirect] { 566 txset[peer] = append(txset[peer], tx.Hash()) 567 } 568 // For the remaining peers, send announcement only 569 for _, peer := range peers[numDirect:] { 570 annos[peer] = append(annos[peer], tx.Hash()) 571 } 572 } 573 for peer, hashes := range txset { 574 directPeers++ 575 directCount += len(hashes) 576 peer.AsyncSendTransactions(hashes) 577 } 578 for peer, hashes := range annos { 579 annoPeers++ 580 annoCount += len(hashes) 581 peer.AsyncSendPooledTransactionHashes(hashes) 582 } 583 log.Debug("Transaction broadcast", "txs", len(txs), 584 "announce packs", annoPeers, "announced hashes", annoCount, 585 "tx packs", directPeers, "broadcast txs", directCount) 586 } 587 588 // minedBroadcastLoop sends mined blocks to connected peers. 589 func (h *handler) minedBroadcastLoop() { 590 defer h.wg.Done() 591 592 for obj := range h.minedBlockSub.Chan() { 593 if ev, ok := obj.Data.(core.NewMinedBlockEvent); ok { 594 h.BroadcastBlock(ev.Block, true) // First propagate block to peers 595 h.BroadcastBlock(ev.Block, false) // Only then announce to the rest 596 } 597 } 598 } 599 600 // txBroadcastLoop announces new transactions to connected peers. 601 func (h *handler) txBroadcastLoop() { 602 defer h.wg.Done() 603 for { 604 select { 605 case event := <-h.txsCh: 606 h.BroadcastTransactions(event.Txs) 607 case <-h.txsSub.Err(): 608 return 609 } 610 } 611 }