github.com/yinchengtsinghua/golang-Eos-dpos-Ethereum@v0.0.0-20190121132951-92cc4225ed8e/p2p/discover/udp.go (about) 1 2 //此源码被清华学神尹成大魔王专业翻译分析并修改 3 //尹成QQ77025077 4 //尹成微信18510341407 5 //尹成所在QQ群721929980 6 //尹成邮箱 yinc13@mails.tsinghua.edu.cn 7 //尹成毕业于清华大学,微软区块链领域全球最有价值专家 8 //https://mvp.microsoft.com/zh-cn/PublicProfile/4033620 9 //版权所有2015 Go Ethereum作者 10 //此文件是Go以太坊库的一部分。 11 // 12 //Go-Ethereum库是免费软件:您可以重新分发它和/或修改 13 //根据GNU发布的较低通用公共许可证的条款 14 //自由软件基金会,或者许可证的第3版,或者 15 //(由您选择)任何更高版本。 16 // 17 //Go以太坊图书馆的发行目的是希望它会有用, 18 //但没有任何保证;甚至没有 19 //适销性或特定用途的适用性。见 20 //GNU较低的通用公共许可证,了解更多详细信息。 21 // 22 //你应该收到一份GNU较低级别的公共许可证副本 23 //以及Go以太坊图书馆。如果没有,请参见<http://www.gnu.org/licenses/>。 24 25 package discover 26 27 import ( 28 "bytes" 29 "container/list" 30 "crypto/ecdsa" 31 "errors" 32 "fmt" 33 "net" 34 "time" 35 36 "github.com/ethereum/go-ethereum/crypto" 37 "github.com/ethereum/go-ethereum/log" 38 "github.com/ethereum/go-ethereum/p2p/nat" 39 "github.com/ethereum/go-ethereum/p2p/netutil" 40 "github.com/ethereum/go-ethereum/rlp" 41 ) 42 43 //错误 44 var ( 45 errPacketTooSmall = errors.New("too small") 46 errBadHash = errors.New("bad hash") 47 errExpired = errors.New("expired") 48 errUnsolicitedReply = errors.New("unsolicited reply") 49 errUnknownNode = errors.New("unknown node") 50 errTimeout = errors.New("RPC timeout") 51 errClockWarp = errors.New("reply deadline too far in the future") 52 errClosed = errors.New("socket closed") 53 ) 54 55 //超时 56 const ( 57 respTimeout = 500 * time.Millisecond 58 expiration = 20 * time.Second 59 60 ntpFailureThreshold = 32 //连续超时,之后检查NTP 61 ntpWarningCooldown = 10 * time.Minute //重复NTP警告之前要经过的最短时间 62 driftThreshold = 10 * time.Second //警告用户前允许的时钟漂移 63 ) 64 65 //RPC数据包类型 66 const ( 67 pingPacket = iota + 1 //零为“保留” 68 pongPacket 69 findnodePacket 70 neighborsPacket 71 ) 72 73 //RPC请求结构 74 type ( 75 ping struct { 76 Version uint 77 From, To rpcEndpoint 78 Expiration uint64 79 //忽略其他字段(为了向前兼容)。 80 Rest []rlp.RawValue `rlp:"tail"` 81 } 82 83 //乒乓球是对乒乓球的回应。 84 pong struct { 85 //此字段应镜像UDP信封地址 86 //提供了一种发现 87 //外部地址(在NAT之后)。 88 To rpcEndpoint 89 90 ReplyTok []byte //这包含ping包的哈希。 91 Expiration uint64 //数据包失效的绝对时间戳。 92 //忽略其他字段(为了向前兼容)。 93 Rest []rlp.RawValue `rlp:"tail"` 94 } 95 96 //findnode是对接近给定目标的节点的查询。 97 findnode struct { 98 Target NodeID //不需要是实际的公钥 99 Expiration uint64 100 //忽略其他字段(为了向前兼容)。 101 Rest []rlp.RawValue `rlp:"tail"` 102 } 103 104 //回复findnode 105 neighbors struct { 106 Nodes []rpcNode 107 Expiration uint64 108 //忽略其他字段(为了向前兼容)。 109 Rest []rlp.RawValue `rlp:"tail"` 110 } 111 112 rpcNode struct { 113 IP net.IP //IPv4的len 4或IPv6的len 16 114 UDP uint16 //用于发现协议 115 TCP uint16 //对于RLPX协议 116 ID NodeID 117 } 118 119 rpcEndpoint struct { 120 IP net.IP //IPv4的len 4或IPv6的len 16 121 UDP uint16 //用于发现协议 122 TCP uint16 //对于RLPX协议 123 } 124 ) 125 126 func makeEndpoint(addr *net.UDPAddr, tcpPort uint16) rpcEndpoint { 127 ip := addr.IP.To4() 128 if ip == nil { 129 ip = addr.IP.To16() 130 } 131 return rpcEndpoint{IP: ip, UDP: uint16(addr.Port), TCP: tcpPort} 132 } 133 134 func (t *udp) nodeFromRPC(sender *net.UDPAddr, rn rpcNode) (*Node, error) { 135 if rn.UDP <= 1024 { 136 return nil, errors.New("low port") 137 } 138 if err := netutil.CheckRelayIP(sender.IP, rn.IP); err != nil { 139 return nil, err 140 } 141 if t.netrestrict != nil && !t.netrestrict.Contains(rn.IP) { 142 return nil, errors.New("not contained in netrestrict whitelist") 143 } 144 n := NewNode(rn.ID, rn.IP, rn.UDP, rn.TCP) 145 err := n.validateComplete() 146 return n, err 147 } 148 149 func nodeToRPC(n *Node) rpcNode { 150 return rpcNode{ID: n.ID, IP: n.IP, UDP: n.UDP, TCP: n.TCP} 151 } 152 153 type packet interface { 154 handle(t *udp, from *net.UDPAddr, fromID NodeID, mac []byte) error 155 name() string 156 } 157 158 type conn interface { 159 ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) 160 WriteToUDP(b []byte, addr *net.UDPAddr) (n int, err error) 161 Close() error 162 LocalAddr() net.Addr 163 } 164 165 //UDP实现RPC协议。 166 type udp struct { 167 conn conn 168 netrestrict *netutil.Netlist 169 priv *ecdsa.PrivateKey 170 ourEndpoint rpcEndpoint 171 172 addpending chan *pending 173 gotreply chan reply 174 175 closing chan struct{} 176 nat nat.Interface 177 178 *Table 179 } 180 181 //挂起表示挂起的答复。 182 // 183 //协议的某些实现希望发送多个 184 //将数据包回复到findnode。一般来说,任何邻居包都不能 185 //与特定的findnode包匹配。 186 // 187 //我们的实现通过存储 188 //每个等待答复。来自节点的传入数据包被调度 189 //到该节点的所有回调函数。 190 type pending struct { 191 //这些字段必须在答复中匹配。 192 from NodeID 193 ptype byte 194 195 //请求必须完成的时间 196 deadline time.Time 197 198 //当匹配的答复到达时调用回调。如果它回来 199 //如果为true,则从挂起的答复队列中删除回调。 200 //如果返回错误,则认为答复不完整,并且 201 //将为下一个匹配的答复再次调用回调。 202 callback func(resp interface{}) (done bool) 203 204 //当回调指示完成或 205 //如果在超时时间内没有收到进一步的答复,则出错。 206 errc chan<- error 207 } 208 209 type reply struct { 210 from NodeID 211 ptype byte 212 data interface{} 213 //循环指示是否存在 214 //通过此频道发送的匹配请求。 215 matched chan<- bool 216 } 217 218 //无法处理readpacket时,会将其发送到未处理的通道。 219 type ReadPacket struct { 220 Data []byte 221 Addr *net.UDPAddr 222 } 223 224 //配置保存与表相关的设置。 225 type Config struct { 226 //需要这些设置并配置UDP侦听器: 227 PrivateKey *ecdsa.PrivateKey 228 229 //这些设置是可选的: 230 AnnounceAddr *net.UDPAddr //DHT中公布的本地地址 231 NodeDBPath string //如果设置,则节点数据库存储在此文件系统位置 232 NetRestrict *netutil.Netlist //网络白名单 233 Bootnodes []*Node //引导程序节点列表 234 Unhandled chan<- ReadPacket //在此通道上发送未处理的数据包 235 } 236 237 //listenudp返回一个新表,用于侦听laddr上的udp包。 238 func ListenUDP(c conn, cfg Config) (*Table, error) { 239 tab, _, err := newUDP(c, cfg) 240 if err != nil { 241 return nil, err 242 } 243 log.Info("UDP listener up", "self", tab.self) 244 return tab, nil 245 } 246 247 func newUDP(c conn, cfg Config) (*Table, *udp, error) { 248 udp := &udp{ 249 conn: c, 250 priv: cfg.PrivateKey, 251 netrestrict: cfg.NetRestrict, 252 closing: make(chan struct{}), 253 gotreply: make(chan reply), 254 addpending: make(chan *pending), 255 } 256 realaddr := c.LocalAddr().(*net.UDPAddr) 257 if cfg.AnnounceAddr != nil { 258 realaddr = cfg.AnnounceAddr 259 } 260 //TODO:单独的TCP端口 261 udp.ourEndpoint = makeEndpoint(realaddr, uint16(realaddr.Port)) 262 tab, err := newTable(udp, PubkeyID(&cfg.PrivateKey.PublicKey), realaddr, cfg.NodeDBPath, cfg.Bootnodes) 263 if err != nil { 264 return nil, nil, err 265 } 266 udp.Table = tab 267 268 go udp.loop() 269 go udp.readLoop(cfg.Unhandled) 270 return udp.Table, udp, nil 271 } 272 273 func (t *udp) close() { 274 close(t.closing) 275 t.conn.Close() 276 //TODO:等待循环结束。 277 } 278 279 //ping向给定节点发送ping消息并等待答复。 280 func (t *udp) ping(toid NodeID, toaddr *net.UDPAddr) error { 281 return <-t.sendPing(toid, toaddr, nil) 282 } 283 284 //发送ping向给定节点发送ping消息并调用回调 285 //当回复到达时。 286 func (t *udp) sendPing(toid NodeID, toaddr *net.UDPAddr, callback func()) <-chan error { 287 req := &ping{ 288 Version: 4, 289 From: t.ourEndpoint, 290 To: makeEndpoint(toaddr, 0), //TODO:可能使用数据库中已知的TCP端口 291 Expiration: uint64(time.Now().Add(expiration).Unix()), 292 } 293 packet, hash, err := encodePacket(t.priv, pingPacket, req) 294 if err != nil { 295 errc := make(chan error, 1) 296 errc <- err 297 return errc 298 } 299 errc := t.pending(toid, pongPacket, func(p interface{}) bool { 300 ok := bytes.Equal(p.(*pong).ReplyTok, hash) 301 if ok && callback != nil { 302 callback() 303 } 304 return ok 305 }) 306 t.write(toaddr, req.name(), packet) 307 return errc 308 } 309 310 func (t *udp) waitping(from NodeID) error { 311 return <-t.pending(from, pingPacket, func(interface{}) bool { return true }) 312 } 313 314 //findnode向给定节点发送findnode请求,并等待直到 315 //节点已发送到k个邻居。 316 func (t *udp) findnode(toid NodeID, toaddr *net.UDPAddr, target NodeID) ([]*Node, error) { 317 //如果我们有一段时间没有看到目标节点的ping,它将不会记得 318 //我们的端点证明和拒绝findnode。先打个乒乓球。 319 if time.Since(t.db.lastPingReceived(toid)) > nodeDBNodeExpiration { 320 t.ping(toid, toaddr) 321 t.waitping(toid) 322 } 323 324 nodes := make([]*Node, 0, bucketSize) 325 nreceived := 0 326 errc := t.pending(toid, neighborsPacket, func(r interface{}) bool { 327 reply := r.(*neighbors) 328 for _, rn := range reply.Nodes { 329 nreceived++ 330 n, err := t.nodeFromRPC(toaddr, rn) 331 if err != nil { 332 log.Trace("Invalid neighbor node received", "ip", rn.IP, "addr", toaddr, "err", err) 333 continue 334 } 335 nodes = append(nodes, n) 336 } 337 return nreceived >= bucketSize 338 }) 339 t.send(toaddr, findnodePacket, &findnode{ 340 Target: target, 341 Expiration: uint64(time.Now().Add(expiration).Unix()), 342 }) 343 return nodes, <-errc 344 } 345 346 //挂起向挂起的答复队列添加答复回调。 347 //有关详细说明,请参阅“挂起”类型的文档。 348 func (t *udp) pending(id NodeID, ptype byte, callback func(interface{}) bool) <-chan error { 349 ch := make(chan error, 1) 350 p := &pending{from: id, ptype: ptype, callback: callback, errc: ch} 351 select { 352 case t.addpending <- p: 353 //循环将处理它 354 case <-t.closing: 355 ch <- errClosed 356 } 357 return ch 358 } 359 360 func (t *udp) handleReply(from NodeID, ptype byte, req packet) bool { 361 matched := make(chan bool, 1) 362 select { 363 case t.gotreply <- reply{from, ptype, req, matched}: 364 //循环将处理它 365 return <-matched 366 case <-t.closing: 367 return false 368 } 369 } 370 371 //循环在自己的Goroutine中运行。它跟踪 372 //刷新计时器和挂起的答复队列。 373 func (t *udp) loop() { 374 var ( 375 plist = list.New() 376 timeout = time.NewTimer(0) 377 nextTimeout *pending //上次重置超时时的plist头 378 contTimeouts = 0 //要执行NTP检查的连续超时数 379 ntpWarnTime = time.Unix(0, 0) 380 ) 381 <-timeout.C //忽略第一次超时 382 defer timeout.Stop() 383 384 resetTimeout := func() { 385 if plist.Front() == nil || nextTimeout == plist.Front().Value { 386 return 387 } 388 //启动计时器,以便在下一个挂起的答复过期时触发。 389 now := time.Now() 390 for el := plist.Front(); el != nil; el = el.Next() { 391 nextTimeout = el.Value.(*pending) 392 if dist := nextTimeout.deadline.Sub(now); dist < 2*respTimeout { 393 timeout.Reset(dist) 394 return 395 } 396 //删除截止时间太长的挂起答复 397 //未来。如果系统时钟跳变,就会发生这种情况。 398 //在最后期限被分配后向后。 399 nextTimeout.errc <- errClockWarp 400 plist.Remove(el) 401 } 402 nextTimeout = nil 403 timeout.Stop() 404 } 405 406 for { 407 resetTimeout() 408 409 select { 410 case <-t.closing: 411 for el := plist.Front(); el != nil; el = el.Next() { 412 el.Value.(*pending).errc <- errClosed 413 } 414 return 415 416 case p := <-t.addpending: 417 p.deadline = time.Now().Add(respTimeout) 418 plist.PushBack(p) 419 420 case r := <-t.gotreply: 421 var matched bool 422 for el := plist.Front(); el != nil; el = el.Next() { 423 p := el.Value.(*pending) 424 if p.from == r.from && p.ptype == r.ptype { 425 matched = true 426 //如果Matcher的回调指示 427 //所有答复都已收到。这是 428 //需要多个数据包类型 429 //应答包。 430 if p.callback(r.data) { 431 p.errc <- nil 432 plist.Remove(el) 433 } 434 //重置连续超时计数器(时间漂移检测) 435 contTimeouts = 0 436 } 437 } 438 r.matched <- matched 439 440 case now := <-timeout.C: 441 nextTimeout = nil 442 443 //通知并删除期限已过的回调。 444 for el := plist.Front(); el != nil; el = el.Next() { 445 p := el.Value.(*pending) 446 if now.After(p.deadline) || now.Equal(p.deadline) { 447 p.errc <- errTimeout 448 plist.Remove(el) 449 contTimeouts++ 450 } 451 } 452 //如果我们累积了太多超时,请执行NTP时间同步检查 453 if contTimeouts > ntpFailureThreshold { 454 if time.Since(ntpWarnTime) >= ntpWarningCooldown { 455 ntpWarnTime = time.Now() 456 go checkClockDrift() 457 } 458 contTimeouts = 0 459 } 460 } 461 } 462 } 463 464 const ( 465 macSize = 256 / 8 466 sigSize = 520 / 8 467 headSize = macSize + sigSize //包帧数据空间 468 ) 469 470 var ( 471 headSpace = make([]byte, headSize) 472 473 //邻居答复通过多个数据包发送到 474 //低于1280字节的限制。我们计算最大数 475 //通过填充一个包直到它变得太大。 476 maxNeighbors int 477 ) 478 479 func init() { 480 p := neighbors{Expiration: ^uint64(0)} 481 maxSizeNode := rpcNode{IP: make(net.IP, 16), UDP: ^uint16(0), TCP: ^uint16(0)} 482 for n := 0; ; n++ { 483 p.Nodes = append(p.Nodes, maxSizeNode) 484 size, _, err := rlp.EncodeToReader(p) 485 if err != nil { 486 //如果发生这种情况,它将被单元测试捕获。 487 panic("cannot encode: " + err.Error()) 488 } 489 if headSize+size+1 >= 1280 { 490 maxNeighbors = n 491 break 492 } 493 } 494 } 495 496 func (t *udp) send(toaddr *net.UDPAddr, ptype byte, req packet) ([]byte, error) { 497 packet, hash, err := encodePacket(t.priv, ptype, req) 498 if err != nil { 499 return hash, err 500 } 501 return hash, t.write(toaddr, req.name(), packet) 502 } 503 504 func (t *udp) write(toaddr *net.UDPAddr, what string, packet []byte) error { 505 _, err := t.conn.WriteToUDP(packet, toaddr) 506 log.Trace(">> "+what, "addr", toaddr, "err", err) 507 return err 508 } 509 510 func encodePacket(priv *ecdsa.PrivateKey, ptype byte, req interface{}) (packet, hash []byte, err error) { 511 b := new(bytes.Buffer) 512 b.Write(headSpace) 513 b.WriteByte(ptype) 514 if err := rlp.Encode(b, req); err != nil { 515 log.Error("Can't encode discv4 packet", "err", err) 516 return nil, nil, err 517 } 518 packet = b.Bytes() 519 sig, err := crypto.Sign(crypto.Keccak256(packet[headSize:]), priv) 520 if err != nil { 521 log.Error("Can't sign discv4 packet", "err", err) 522 return nil, nil, err 523 } 524 copy(packet[macSize:], sig) 525 //将哈希添加到前面。注意:这不保护 526 //以任何方式打包。我们的公钥将是这个哈希的一部分 527 //未来。 528 hash = crypto.Keccak256(packet[macSize:]) 529 copy(packet, hash) 530 return packet, hash, nil 531 } 532 533 //readloop在自己的goroutine中运行。它处理传入的UDP数据包。 534 func (t *udp) readLoop(unhandled chan<- ReadPacket) { 535 defer t.conn.Close() 536 if unhandled != nil { 537 defer close(unhandled) 538 } 539 //发现数据包被定义为不大于1280字节。 540 //大于此尺寸的包装将在末端切割并处理 541 //因为它们的哈希不匹配而无效。 542 buf := make([]byte, 1280) 543 for { 544 nbytes, from, err := t.conn.ReadFromUDP(buf) 545 if netutil.IsTemporaryError(err) { 546 //忽略临时读取错误。 547 log.Debug("Temporary UDP read error", "err", err) 548 continue 549 } else if err != nil { 550 //关闭永久错误循环。 551 log.Debug("UDP read error", "err", err) 552 return 553 } 554 if t.handlePacket(from, buf[:nbytes]) != nil && unhandled != nil { 555 select { 556 case unhandled <- ReadPacket{buf[:nbytes], from}: 557 default: 558 } 559 } 560 } 561 } 562 563 func (t *udp) handlePacket(from *net.UDPAddr, buf []byte) error { 564 packet, fromID, hash, err := decodePacket(buf) 565 if err != nil { 566 log.Debug("Bad discv4 packet", "addr", from, "err", err) 567 return err 568 } 569 err = packet.handle(t, from, fromID, hash) 570 log.Trace("<< "+packet.name(), "addr", from, "err", err) 571 return err 572 } 573 574 func decodePacket(buf []byte) (packet, NodeID, []byte, error) { 575 if len(buf) < headSize+1 { 576 return nil, NodeID{}, nil, errPacketTooSmall 577 } 578 hash, sig, sigdata := buf[:macSize], buf[macSize:headSize], buf[headSize:] 579 shouldhash := crypto.Keccak256(buf[macSize:]) 580 if !bytes.Equal(hash, shouldhash) { 581 return nil, NodeID{}, nil, errBadHash 582 } 583 fromID, err := recoverNodeID(crypto.Keccak256(buf[headSize:]), sig) 584 if err != nil { 585 return nil, NodeID{}, hash, err 586 } 587 var req packet 588 switch ptype := sigdata[0]; ptype { 589 case pingPacket: 590 req = new(ping) 591 case pongPacket: 592 req = new(pong) 593 case findnodePacket: 594 req = new(findnode) 595 case neighborsPacket: 596 req = new(neighbors) 597 default: 598 return nil, fromID, hash, fmt.Errorf("unknown type: %d", ptype) 599 } 600 s := rlp.NewStream(bytes.NewReader(sigdata[1:]), 0) 601 err = s.Decode(req) 602 return req, fromID, hash, err 603 } 604 605 func (req *ping) handle(t *udp, from *net.UDPAddr, fromID NodeID, mac []byte) error { 606 if expired(req.Expiration) { 607 return errExpired 608 } 609 t.send(from, pongPacket, &pong{ 610 To: makeEndpoint(from, req.From.TCP), 611 ReplyTok: mac, 612 Expiration: uint64(time.Now().Add(expiration).Unix()), 613 }) 614 t.handleReply(fromID, pingPacket, req) 615 616 //将节点添加到表中。在这样做之前,确保我们最近有足够的乒乓球 617 //记录在数据库中,以便稍后接受其findnode请求。 618 n := NewNode(fromID, from.IP, uint16(from.Port), req.From.TCP) 619 if time.Since(t.db.lastPongReceived(fromID)) > nodeDBNodeExpiration { 620 t.sendPing(fromID, from, func() { t.addThroughPing(n) }) 621 } else { 622 t.addThroughPing(n) 623 } 624 t.db.updateLastPingReceived(fromID, time.Now()) 625 return nil 626 } 627 628 func (req *ping) name() string { return "PING/v4" } 629 630 func (req *pong) handle(t *udp, from *net.UDPAddr, fromID NodeID, mac []byte) error { 631 if expired(req.Expiration) { 632 return errExpired 633 } 634 if !t.handleReply(fromID, pongPacket, req) { 635 return errUnsolicitedReply 636 } 637 t.db.updateLastPongReceived(fromID, time.Now()) 638 return nil 639 } 640 641 func (req *pong) name() string { return "PONG/v4" } 642 643 func (req *findnode) handle(t *udp, from *net.UDPAddr, fromID NodeID, mac []byte) error { 644 if expired(req.Expiration) { 645 return errExpired 646 } 647 if !t.db.hasBond(fromID) { 648 //不存在端点验证pong,我们不处理数据包。这可以防止 649 //攻击向量,发现协议可用于放大 650 //DDoS攻击。恶意参与者将使用IP地址发送findnode请求 651 //目标的UDP端口作为源地址。findnode的接收者 652 //然后,包将发送一个邻居包(比 653 //找到受害者。 654 return errUnknownNode 655 } 656 target := crypto.Keccak256Hash(req.Target[:]) 657 t.mutex.Lock() 658 closest := t.closest(target, bucketSize).entries 659 t.mutex.Unlock() 660 661 p := neighbors{Expiration: uint64(time.Now().Add(expiration).Unix())} 662 var sent bool 663 //以块形式发送邻居,每个数据包最多有maxneighbors 664 //低于1280字节的限制。 665 for _, n := range closest { 666 if netutil.CheckRelayIP(from.IP, n.IP) == nil { 667 p.Nodes = append(p.Nodes, nodeToRPC(n)) 668 } 669 if len(p.Nodes) == maxNeighbors { 670 t.send(from, neighborsPacket, &p) 671 p.Nodes = p.Nodes[:0] 672 sent = true 673 } 674 } 675 if len(p.Nodes) > 0 || !sent { 676 t.send(from, neighborsPacket, &p) 677 } 678 return nil 679 } 680 681 func (req *findnode) name() string { return "FINDNODE/v4" } 682 683 func (req *neighbors) handle(t *udp, from *net.UDPAddr, fromID NodeID, mac []byte) error { 684 if expired(req.Expiration) { 685 return errExpired 686 } 687 if !t.handleReply(fromID, neighborsPacket, req) { 688 return errUnsolicitedReply 689 } 690 return nil 691 } 692 693 func (req *neighbors) name() string { return "NEIGHBORS/v4" } 694 695 func expired(ts uint64) bool { 696 return time.Unix(int64(ts), 0).Before(time.Now()) 697 }