以太坊源码之--P2P网络源码剖析(2)

ethereum-p2p(2)节点发现机制代码分析(v1.8.24)

1 、引导

​ 此部分主要分析以太坊节点发现机制源码,以太坊节点发现部分主要借助了一种分布式哈希表的结构(DHT),Kademlia协议是以太坊节点发现机制的基础,它是一种以节点id异或后的结果作为两节点逻辑距离的一种协议,详细介绍在另一部分。下面主要介绍一下以太坊对此协议的具体实现。

先介绍下主要的数据结构:

// udp主要由四种互相交互的包,如下
// RPC packet types
const (
	pingPacket = iota + 1 // zero is 'reserved'
	pongPacket
	findnodePacket
	neighborsPacket
)

// RPC request structures
type (
    // ping包的定义
	ping struct {
		senderKey *ecdsa.PublicKey // filled in by preverify

		Version    uint
		From, To   rpcEndpoint
		Expiration uint64
		// Ignore additional fields (for forward compatibility).
		Rest []rlp.RawValue `rlp:"tail"`
	}
	// pong包的定义
	// pong is the reply to ping.
	pong struct {
		// This field should mirror the UDP envelope address
		// of the ping packet, which provides a way to discover the
		// the external address (after NAT).
		To rpcEndpoint
		//包含ping包的hash值   方便做验证
		ReplyTok   []byte // This contains the hash of the ping packet.
		Expiration uint64 // Absolute timestamp at which the packet becomes invalid.
		// Ignore additional fields (for forward compatibility).
		Rest []rlp.RawValue `rlp:"tail"`
	}
	// findnode包定义,就是向邻居查找节点的包
	// findnode is a query for nodes close to the given target.
	findnode struct {
		Target     encPubkey
		Expiration uint64
		// Ignore additional fields (for forward compatibility).
		Rest []rlp.RawValue `rlp:"tail"`
	}
	// 这个包是findnode的回复包
	// reply to findnode
	neighbors struct {
		Nodes      []rpcNode
		Expiration uint64
		// Ignore additional fields (for forward compatibility).
		Rest []rlp.RawValue `rlp:"tail"`
	}

	rpcNode struct {
		IP  net.IP // len 4 for IPv4 or 16 for IPv6
		UDP uint16 // for discovery protocol
		TCP uint16 // for RLPx protocol
		ID  encPubkey
	}
	....................
)

下面主要涉及两个代码源文件:udp.go和table.go

1.1 go-ethereum/p2p/discover/udp.go
// 代码分析从server.Start()中开始
------------------------------------------------------------------------
    f err := srv.setupDiscovery(); err != nil {
		return err
	}
------------------------------------------------------------------------
func (srv *Server) setupDiscovery() error {
	// 如果配置参数指定不开启节点发现机制  则直接返回
	if srv.NoDiscovery && !srv.DiscoveryV5 {
		return nil
	}
	// 解析地址
	addr, err := net.ResolveUDPAddr("udp", srv.ListenAddr)
	if err != nil {
		return err
	}
	//借用net包开启实际的udp监听
	conn, err := net.ListenUDP("udp", addr)
	if err != nil {
		return err
	}
	realaddr := conn.LocalAddr().(*net.UDPAddr)
	srv.log.Debug("UDP listener up", "addr", realaddr)
	if srv.NAT != nil {
		if !realaddr.IP.IsLoopback() {
			go nat.Map(srv.NAT, srv.quit, "udp", realaddr.Port, realaddr.Port, "ethereum discovery")
		}
	}
	srv.localnode.SetFallbackUDP(realaddr.Port)
	// 发现协议v4 一般全节点使用这个
	// Discovery V4
	var unhandled chan discover.ReadPacket
	var sconn *sharedUDPConn
	if !srv.NoDiscovery {
		if srv.DiscoveryV5 {
			unhandled = make(chan discover.ReadPacket, 100)
			sconn = &sharedUDPConn{conn, unhandled}
		}
		cfg := discover.Config{
			PrivateKey:  srv.PrivateKey,
			NetRestrict: srv.NetRestrict,
			Bootnodes:   srv.BootstrapNodes,
			Unhandled:   unhandled,
		}
        // 此处实际开启监听
		ntab, err := discover.ListenUDP(conn, srv.localnode, cfg)
		if err != nil {
			return err
		}
		srv.ntab = ntab
	}
    // 以下部分忽略
.............................
}
// ListenUDP returns a new table that listens for UDP packets on laddr.
func ListenUDP(c conn, ln *enode.LocalNode, cfg Config) (*Table, error) {
    // 调用newUDP方法开启udp服务
	tab, _, err := newUDP(c, ln, cfg)
	if err != nil {
		return nil, err
	}
	return tab, nil
}
unc newUDP(c conn, ln *enode.LocalNode, cfg Config) (*Table, *udp, error) {
	udp := &udp{
		conn:            c,
		priv:            cfg.PrivateKey,
		netrestrict:     cfg.NetRestrict,
		localNode:       ln,
		db:              ln.Database(),
		closing:         make(chan struct{}),
		gotreply:        make(chan reply),
		addReplyMatcher: make(chan *replyMatcher),
	}
	//这里的newTabl方法主要去实现了kad协议,这里暂时不分析,放在后面第二部分,这里主要先分析udp部分做的事情
	tab, err := newTable(udp, ln.Database(), cfg.Bootnodes)
	if err != nil {
		return nil, nil, err
	}
	udp.tab = tab

	udp.wg.Add(2)
     // 下面是udp部分的主要两个循环,使用两个协程去做
	//-----------------------------------------------------------
	//  udp.loop()主要用来丢掉过期的包 和从各种数据通道读取相关数据做处理
	//  udp.readLoop()主要读取udp端口的数据,将数据解码为相应的四个包 并分发到相应的处理器进行处理 相应处理器再把结果推给数据通道
	//
	//-----------------------------------------------------------
	//对各种通道和包是否过期进行处理
	go udp.loop()
	//  读取udp的数据包
	go udp.readLoop(cfg.Unhandled)
	return udp.tab, udp, nil
}

下面主要分析udp的两个循环结构loop()和readLoop()

// loop runs in its own goroutine. it keeps track of
// the refresh timer and the pending reply queue.
func (t *udp) loop() {
	defer t.wg.Done()

	var (
		plist        = list.New()
		timeout      = time.NewTimer(0)
		nextTimeout  *replyMatcher // head of plist when timeout was last reset
		// 持续过期包计数器
        contTimeouts = 0           // number of continuous timeouts to do NTP checks
		ntpWarnTime  = time.Unix(0, 0)
	)
	<-timeout.C // ignore first timeout
	defer timeout.Stop()
	// 定义重设过期函数
	resetTimeout := func() {
        // 若list头为空或者下一个过期的包是plist里面的头,那么直接返回
		if plist.Front() == nil || nextTimeout == plist.Front().Value {
			return
		}
		// Start the timer so it fires when the next pending reply has expired.
		// 记录当前时间
        now := time.Now()
         // 从头遍历plist
		for el := plist.Front(); el != nil; el = el.Next() {
			nextTimeout = el.Value.(*replyMatcher)
             // 如果一个包的deadline减去当前时间小于2倍respTimeout (相当于1s)
			if dist := nextTimeout.deadline.Sub(now); dist < 2*respTimeout {
                 //那么重设倒计时计时器的时间为dist时间后到期
				timeout.Reset(dist)
				return
			}
             // 要不然发出一个时钟错误的消息,并且将当前的包移除
             // 因为此时的deadline时间太长
			// Remove pending replies whose deadline is too far in the
			// future. These can occur if the system clock jumped
			// backwards after the deadline was assigned.
			nextTimeout.errc <- errClockWarp
			plist.Remove(el)
		}
		nextTimeout = nil
		timeout.Stop()
	}
	// 此处是udp的主循环 每次循环进来先调用一次resetTimeout()函数
	for {
		resetTimeout()
		// 下面主要接收各种通道发来的消息并进行处理
		select {
         // 若收到关闭消息,那么遍历plist,逐个发送errClosed消息通知关闭
		case <-t.closing:
			for el := plist.Front(); el != nil; el = el.Next() {
				el.Value.(*replyMatcher).errc <- errClosed
			}
			return
 // ------------------------以下两个通道比较重要---------------------------------
		// 将一个待匹配的匹配器添加到plist里面等待消息匹配
		case p := <-t.addReplyMatcher:
			p.deadline = time.Now().Add(respTimeout)
			plist.PushBack(p)
		//将收到的消息回复推送到这里
		case r := <-t.gotreply:
			var matched bool // whether any replyMatcher considered the reply acceptable.
			// 从头遍历plist 看是否和远方的回复包匹配,如匹配则调用相应的callback并传入回复的数据
			for el := plist.Front(); el != nil; el = el.Next() {
				p := el.Value.(*replyMatcher)
                  // 若包的发送方,包类型,ip地址相等,那么认为有一个replyMatcher得到了回复,调用其callback方法
				if p.from == r.from && p.ptype == r.ptype && p.ip.Equal(r.ip) {
					ok, requestDone := p.callback(r.data)
					matched = matched || ok
					// Remove the matcher if callback indicates that all replies have been received.
					if requestDone {
						p.errc <- nil
						plist.Remove(el)
					}
					// Reset the continuous timeout counter (time drift detection)
					contTimeouts = 0
				}
			}
			r.matched <- matched
 // ----------------------------------------------------------------------------
		// 定时进行过期检查
		case now := <-timeout.C:
			nextTimeout = nil
             // 遍历plist如果一个包的deadline小于等于当前时间,那么将其移除
			// Notify and remove callbacks whose deadline is in the past.
			for el := plist.Front(); el != nil; el = el.Next() {
				p := el.Value.(*replyMatcher)
				if now.After(p.deadline) || now.Equal(p.deadline) {
					p.errc <- errTimeout
					plist.Remove(el)
					contTimeouts++
				}
			}
			// 若等待回复的列表出现连续32个过期的包,开始使用ntp进行时钟同步
			// If we've accumulated too many timeouts, do an NTP time sync check
			if contTimeouts > ntpFailureThreshold {
				if time.Since(ntpWarnTime) >= ntpWarningCooldown {
					ntpWarnTime = time.Now()
					go checkClockDrift()
				}
				contTimeouts = 0
			}
		}
	}
}

上面是udp的loop循环,主要做了如下几件事:

  • 首先处理超时信息
  • 接收各种通道来的消息,对消息进行相应处理

下面开看第二个循环readLoop()的分析

// readLoop runs in its own goroutine. it handles incoming UDP packets.
func (t *udp) readLoop(unhandled chan<- ReadPacket) {
	defer t.wg.Done()
	if unhandled != nil {
		defer close(unhandled)
	}
	// discover 包最大不超过1280字节超过以后就非法
	// Discovery packets are defined to be no larger than 1280 bytes.
	// Packets larger than this size will be cut at the end and treated
	// as invalid because their hash won't match.
	buf := make([]byte, 1280)
    // for无限循环从udp读取数据
	for {
        //
		nbytes, from, err := t.conn.ReadFromUDP(buf)
		if netutil.IsTemporaryError(err) {
			// Ignore temporary read errors.
			log.Debug("Temporary UDP read error", "err", err)
			continue
		} else if err != nil {
			// Shut down the loop for permament errors.
			log.Debug("UDP read error", "err", err)
			return
		}
        // 将读取的数据交给handlePacket处理
		if t.handlePacket(from, buf[:nbytes]) != nil && unhandled != nil {
			select {
			case unhandled <- ReadPacket{buf[:nbytes], from}:
			default:
			}
		}
	}
}
// 这个函数主要用来处理从udp端口收到的数据
func (t *udp) handlePacket(from *net.UDPAddr, buf []byte) error {
    // 先交给decodePacket函数进行捷报,将包解包为四种包的一种
	packet, fromKey, hash, err := decodePacket(buf)
	if err != nil {
		log.Debug("Bad discv4 packet", "addr", from, "err", err)
		return err
	}
	fromID := fromKey.id()
	if err == nil {
        // 在此调用了一下对应包的预验证
		err = packet.preverify(t, from, fromID, fromKey)
	}
	log.Trace("<< "+packet.name(), "id", fromID, "addr", from, "err", err)
	if err == nil {
        // 无误的时候调用相应包的handle处理相应消息
		packet.handle(t, from, fromID, hash)
	}
	return err
}
// decodePacket方法
func decodePacket(buf []byte) (packet, encPubkey, []byte, error) {
	if len(buf) < headSize+1 {
		return nil, encPubkey{}, nil, errPacketTooSmall
	}
    // 将数据包的数据分开
	hash, sig, sigdata := buf[:macSize], buf[macSize:headSize], buf[headSize:]
	shouldhash := crypto.Keccak256(buf[macSize:])
	if !bytes.Equal(hash, shouldhash) {
		return nil, encPubkey{}, nil, errBadHash
	}
	fromKey, err := recoverNodeKey(crypto.Keccak256(buf[headSize:]), sig)
	if err != nil {
		return nil, fromKey, hash, err
	}
	var req packet
    //判断包的类型,根据类型创建相应的包
	switch ptype := sigdata[0]; ptype {
	case pingPacket:
		req = new(ping)
	case pongPacket:
		req = new(pong)
	case findnodePacket:
		req = new(findnode)
	case neighborsPacket:
		req = new(neighbors)
	default:
		return nil, fromKey, hash, fmt.Errorf("unknown type: %d", ptype)
	}
	s := rlp.NewStream(bytes.NewReader(sigdata[1:]), 0)
	err = s.Decode(req)
	return req, fromKey, hash, err
}

上面就是udp.readLoop()的相应代码分析,主要任务就是从udp端口读取数据,将数据解成四种包的一种,然后调用其preverify方法进行预验证,验证通过后交给相应包的handle函数处理。

下面分析下四种包的预验证和处理函数

ping包:

// Packet Handlers
func (req *ping) preverify(t *udp, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error {
    // 首先检查是否过期
	if expired(req.Expiration) {
		return errExpired
	}
    // 根据发送方加密公钥解出公钥,确保可以正确解出
	key, err := decodePubkey(fromKey)
	if err != nil {
		return errors.New("invalid public key")
	}
	req.senderKey = key
	return nil
}
//如果收到一个 ping 请求 判断是否收到上一次此节点的pong请求是否过期,一般是24小时 ,如果过期则回发ping包  并等待到pong回复时候 将对方节点加入k桶
func (req *ping) handle(t *udp, from *net.UDPAddr, fromID enode.ID, mac []byte) {
	// Reply.
    // 当收到Ping包时候,先向对方发送pong包
	t.send(from, fromID, pongPacket, &pong{
		To:         makeEndpoint(from, req.From.TCP),
		ReplyTok:   mac,
		Expiration: uint64(time.Now().Add(expiration).Unix()),
	})

	// Ping back if our last pong on file is too far in the past.
    // 根据对方请求携带的消息,将相应消息包装为一个node结构
	n := wrapNode(enode.NewV4(req.senderKey, from.IP, int(req.From.TCP), from.Port))
     // 查看数据库上次收到对方pong回应的时间是否大于24小时,若是则向对方发送ping包,并将对方加入k桶,k桶概念下节分析。若小于24小时,直接将对方加入到k桶
	if time.Since(t.db.LastPongReceived(n.ID(), from.IP)) > bondExpiration {
		t.sendPing(fromID, from, func() {
			t.tab.addVerifiedNode(n)
		})
	} else {
		t.tab.addVerifiedNode(n)
	}

	// Update node database and endpoint predictor.
	t.db.UpdateLastPingReceived(n.ID(), from.IP, time.Now())
	t.localNode.UDPEndpointStatement(from, &net.UDPAddr{IP: req.To.IP, Port: int(req.To.UDP)})
}

pong包:

// pong包的预验证逻辑
func (req *pong) preverify(t *udp, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error {
    // 判断是否过期
	if expired(req.Expiration) {
		return errExpired
	}
    // 将pong包交给t.gotreply通道,看是否有一个匹配到,若没有匹配结果则返回错误
	if !t.handleReply(fromID, from.IP, pongPacket, req) {
		return errUnsolicitedReply
	}
	return nil
}
// pong包的handle函数
func (req *pong) handle(t *udp, from *net.UDPAddr, fromID enode.ID, mac []byte) {
	t.localNode.UDPEndpointStatement(from, &net.UDPAddr{IP: req.To.IP, Port: int(req.To.UDP)})
     // 简单更新下数据库中收到from节点pong包的时间
	t.db.UpdateLastPongReceived(fromID, from.IP, time.Now())
}

findnode包:

func (req *findnode) preverify(t *udp, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error {
    // 判断是否过期
	if expired(req.Expiration) {
		return errExpired
	}
    // 如果发送查找节点包的节点上次回复我们pong消息大于24小时,那么直接返回错误
	if time.Since(t.db.LastPongReceived(fromID, from.IP)) > bondExpiration {
		// No endpoint proof pong exists, we don't process the packet. This prevents an
		// attack vector where the discovery protocol could be used to amplify traffic in a
		// DDOS attack. A malicious actor would send a findnode request with the IP address
		// and UDP port of the target as the source address. The recipient of the findnode
		// packet would then send a neighbors packet (which is a much bigger packet than
		// findnode) to the victim.
		return errUnknownNode
	}
	return nil
}

func (req *findnode) handle(t *udp, from *net.UDPAddr, fromID enode.ID, mac []byte) {
	// Determine closest nodes.
	target := enode.ID(crypto.Keccak256Hash(req.Target[:]))
	t.tab.mutex.Lock()
     // 从自己的bucket里面查找bucketSize个离目标节点最近的
	closest := t.tab.closest(target, bucketSize).entries
	t.tab.mutex.Unlock()

	// Send neighbors in chunks with at most maxNeighbors per packet
	// to stay below the 1280 byte limit.
	p := neighbors{Expiration: uint64(time.Now().Add(expiration).Unix())}
	var sent bool
	for _, n := range closest {
		if netutil.CheckRelayIP(from.IP, n.IP()) == nil {
			p.Nodes = append(p.Nodes, nodeToRPC(n))
		}
        // 当添加的节点超过1280个字节时候开始发送回包
		if len(p.Nodes) == maxNeighbors {
                t.send(from, fromID, neighborsPacket, &p)
			p.Nodes = p.Nodes[:0]
			sent = true
		}
	}
    
    // 如果上面查找到的结果没超过neighbors包的限制,那么使用下面的逻辑再发送
	if len(p.Nodes) > 0 || !sent {
		t.send(from, fromID, neighborsPacket, &p)
	}
}

neighbors包:

func (req *neighbors) preverify(t *udp, from *net.UDPAddr, fromID enode.ID, fromKey encPubkey) error {
	if expired(req.Expiration) {
		return errExpired
	}
    // 直接交给匹配通道进行匹配
	if !t.handleReply(fromID, from.IP, neighborsPacket, req) {
		return errUnsolicitedReply
	}
	return nil
}

func (req *neighbors) handle(t *udp, from *net.UDPAddr, fromID enode.ID, mac []byte) {
}

再来分析一个借助udp包发送ping请求的整个流程来分析它的匹配回调机制

// sendPing sends a ping message to the given node and invokes the callback
// when the reply arrives.
func (t *udp) sendPing(toid enode.ID, toaddr *net.UDPAddr, callback func()) <-chan error {
	// 先封装一个ping请求
    req := &ping{
        // 版本号
		Version:    4,
        // 发送方
		From:       t.ourEndpoint(),
        // 接收方
		To:         makeEndpoint(toaddr, 0), // TODO: maybe use known TCP port from DB
		Expiration: uint64(time.Now().Add(expiration).Unix()),
	}
    // 对包进行编码
	packet, hash, err := encodePacket(t.priv, pingPacket, req)
	if err != nil {
		errc := make(chan error, 1)
		errc <- err
		return errc
	}
	// Add a matcher for the reply to the pending reply queue. Pongs are matched if they
	// reference the ping we're about to send.
    // 开始将包组装成一个replyMatcher放入plist
	errc := t.pending(toid, toaddr.IP, pongPacket, func(p interface{}) (matched bool, requestDone bool) {
		matched = bytes.Equal(p.(*pong).ReplyTok, hash)
		if matched && callback != nil {
			callback()
		}
		return matched, matched
	})
	// Send the packet.
	t.localNode.UDPContact(toaddr)
     // 最后发送包
	t.write(toaddr, toid, req.name(), packet)
	return errc
}

// pending adds a reply matcher to the pending reply queue.
// see the documentation of type replyMatcher for a detailed explanation.
func (t *udp) pending(id enode.ID, ip net.IP, ptype byte, callback replyMatchFunc) <-chan error {
	ch := make(chan error, 1)
     // 根据包的信息组装为一个replyMatcher并放入addReplyMatcher通道,最后等待t.gotreply有消息收到后进行匹配,看是不是此消息的回复
	p := &replyMatcher{from: id, ip: ip, ptype: ptype, callback: callback, errc: ch}
	select {
	case t.addReplyMatcher <- p:
		// loop will handle it
	case <-t.closing:
		ch <- errClosed
	}
	return ch
}

以上就是udp里面的主要循环,主要有loop()主要做的工作是将过期的包删除,处理各种udp相关通道的消息readLoop()主要循环从udp端口读取消息,将消息解码成相应包,调用相应包的处理函数,并将消息推送给相关通道。最后分析了下节点如何利用udp发送包,并把回应包放入plist,若正确收到回复,则调用相应包的callback()函数。

1.2 go-ethereum/p2p/discover/table.go

先介绍下table下的相关数据结构

const (
	alpha           = 3  // Kademlia concurrency factor
	bucketSize      = 16 // Kademlia bucket size bucket单个桶大小
	maxReplacements = 10 // Size of per-bucket replacement list //最大替换数

	// We keep buckets for the upper 1/15 of distances because
	// it's very unlikely we'll ever encounter a node that's closer.
	hashBits          = len(common.Hash{}) * 8 //256
	nBuckets          = hashBits / 15       // Number of buckets 17 bucket个数
	bucketMinDistance = hashBits - nBuckets // Log distance of closest bucket bucket 0层和1层之间的距离

	// IP address limits.
	bucketIPLimit, bucketSubnet = 2, 24 // at most 2 addresses from the same /24
	tableIPLimit, tableSubnet   = 10, 24

	maxFindnodeFailures = 5 // Nodes exceeding this limit are dropped 发现节点最大失败次数
	refreshInterval     = 30 * time.Minute // 刷新table间隙
	revalidateInterval  = 10 * time.Second //做最后节点验证间隙
	copyNodesInterval   = 30 * time.Second // 将liveness大于1的节点拷贝到数据nodes目录的间隙
	seedMinTableTime    = 5 * time.Minute
	seedCount           = 30 // 种子节点数量
	seedMaxAge          = 5 * 24 * time.Hour 
)
// table数据结构定义
type Table struct {
	mutex   sync.Mutex        // protects buckets, bucket content, nursery, rand
	buckets [nBuckets]*bucket // index of known nodes by distance bucket数组定义
	nursery []*node           // bootstrap nodes 引导节点切片
	rand    *mrand.Rand       // source of randomness, periodically reseeded 
	ips     netutil.DistinctNetSet

	db         *enode.DB // database of known nodes 使用db将已知节点存入nodes
	net        transport
	refreshReq chan chan struct{}
	initDone   chan struct{}

	closeOnce sync.Once
	closeReq  chan struct{}
	closed    chan struct{}

	nodeAddedHook func(*node) // for testing
}

下面开始分析newTable()方法的逻辑

func newTable(t transport, db *enode.DB, bootnodes []*enode.Node) (*Table, error) {
	tab := &Table{
		net:        t,// udp实现了transport接口的方法,table借用udp进行数据传输
		db:         db,
		refreshReq: make(chan chan struct{}), // table刷新通道
		initDone:   make(chan struct{}),
		closeReq:   make(chan struct{}),
		closed:     make(chan struct{}),
		rand:       mrand.New(mrand.NewSource(0)),
		ips:        netutil.DistinctNetSet{Subnet: tableSubnet, Limit: tableIPLimit}, // 24 ,10
	}
	if err := tab.setFallbackNodes(bootnodes); err != nil {
		return nil, err
	}
    // 开始将17个bucket进行变量初始化
	for i := range tab.buckets {
		tab.buckets[i] = &bucket{
			ips: netutil.DistinctNetSet{Subnet: bucketSubnet, Limit: bucketIPLimit},
		}
	}
	// 读取一个随机数种子
	tab.seedRand()
	tab.loadSeedNodes()
	//进行table循环  主要做doRefresh    revalidate  copyNodes
	go tab.loop()
	return tab, nil
}
// 下面是table主循环代码
// loop schedules refresh, revalidate runs and coordinates shutdown.
func (tab *Table) loop() {
	var (
        // 首先创建三个定时器,主要功能下面讲解
		revalidate     = time.NewTimer(tab.nextRevalidateTime())
		refresh        = time.NewTicker(refreshInterval)
		copyNodes      = time.NewTicker(copyNodesInterval)
		refreshDone    = make(chan struct{})           // where doRefresh reports completion
		revalidateDone chan struct{}                   // where doRevalidate reports completion
		waiting        = []chan struct{}{tab.initDone} // holds waiting callers while doRefresh runs
	)
	defer refresh.Stop()
	defer revalidate.Stop()
	defer copyNodes.Stop()

	// Start initial refresh.
	go tab.doRefresh(refreshDone)

loop:
	for {
		select {
		// 如果到了刷新时间,则进行table刷新 30分钟一次
		case <-refresh.C:
			tab.seedRand()
			if refreshDone == nil {
				refreshDone = make(chan struct{})
				go tab.doRefresh(refreshDone)
			}
		case req := <-tab.refreshReq:
			waiting = append(waiting, req)
			if refreshDone == nil {
				refreshDone = make(chan struct{})
				go tab.doRefresh(refreshDone)
			}
		case <-refreshDone:
			for _, ch := range waiting {
				close(ch)
			}
			waiting, refreshDone = nil, nil
		//到了验证随机桶里最后一个节点是否存活  主要保持桶鲜活 10秒一次
		case <-revalidate.C:
			revalidateDone = make(chan struct{})
			go tab.doRevalidate(revalidateDone)
		case <-revalidateDone:
			revalidate.Reset(tab.nextRevalidateTime())
			revalidateDone = nil
		// 30 秒一次  将所有 桶里盼活计数大于0 的节点存到数据库
		case <-copyNodes.C:
			go tab.copyLiveNodes()
		case <-tab.closeReq:
			break loop
		}
	}

	if refreshDone != nil {
		<-refreshDone
	}
	for _, ch := range waiting {
		close(ch)
	}
	if revalidateDone != nil {
		<-revalidateDone
	}
	close(tab.closed)
}


// doRefresh performs a lookup for a random target to keep buckets
// full. seed nodes are inserted if the table is empty (initial
// bootstrap or discarded faulty peers).
func (tab *Table) doRefresh(done chan struct{}) {
	defer close(done)
	//从数据库加载那些被存在数据库中且仍然存活的节点
	// Load nodes from the database and insert
	// them. This should yield a few previously seen nodes that are
	// (hopefully) still alive.
	tab.loadSeedNodes()
	//先进行一次自我查找 为了让别人尽可能把自己加到bucket里面
	// Run self lookup to discover new neighbor nodes.
	// We can only do this if we have a secp256k1 identity.
	var key ecdsa.PublicKey
	if err := tab.self().Load((*enode.Secp256k1)(&key)); err == nil {
		tab.lookup(encodePubkey(&key), false)
	}

	// The Kademlia paper specifies that the bucket refresh should
	// perform a lookup in the least recently used bucket. We cannot
	// adhere to this because the findnode target is a 512bit value
	// (not hash-sized) and it is not easily possible to generate a
	// sha3 preimage that falls into a chosen bucket.
	// We perform a few lookups with a random target instead.
	// 查询了三个随机节点 ,这三个随机节点不一定存在
	for i := 0; i < 3; i++ {
		var target encPubkey
		crand.Read(target[:])
		tab.lookup(target, false)
	}
}

// lookup performs a network search for nodes close to the given target. It approaches the
// target by querying nodes that are closer to it on each iteration. The given target does
// not need to be an actual node identifier.
func (tab *Table) lookup(targetKey encPubkey, refreshIfEmpty bool) []*node {
	var (
        // 目标节点
		target         = enode.ID(crypto.Keccak256Hash(targetKey[:]))
		// 本次lookup询问过的节点
        asked          = make(map[enode.ID]bool)
		seen           = make(map[enode.ID]bool)
        // 返回结果
		reply          = make(chan []*node, alpha)
		// 处于pending的查询
        pendingQueries = 0
        // 本次lookup的结果
		result         *nodesByDistance
	)
	// 先设置自己为已经询问过的节点
	// don't query further if we hit ourself.
	// unlikely to happen often in practice.
	asked[tab.self().ID()] = true

	for {
		tab.mutex.Lock()
		//  从本地桶里拿出来16个最近的节点返回,根据nodeid进行异或的逻辑距离
		// generate initial result set
		result = tab.closest(target, bucketSize)
		tab.mutex.Unlock()
		//
		if len(result.entries) > 0 || !refreshIfEmpty {
			break
		}
		// The result set is empty, all nodes were dropped, refresh.
		// We actually wait for the refresh to complete here. The very
		// first query will hit this case and run the bootstrapping
		// logic.
		<-tab.refresh()
		refreshIfEmpty = false
	}
	// 以下for循环按照逻辑来看,会将本地离target最近的16个节点一一发送findnode包,将返回的结果(若全部返回正常符合条件的结果来看是16*16个)里面再拿出来离target最近的16个放入result里面
	for {
        // 遍历本地查找到离target距离近的节点
		// ask the alpha closest nodes that we haven't asked yet
		for i := 0; i < len(result.entries) && pendingQueries < alpha; i++ {
			n := result.entries[i]
             // 如果节点未询问
			if !asked[n.ID()] {
				// 将此节点标位已经询问
                  asked[n.ID()] = true
                 // pending计数递增
				pendingQueries++
                  // 开启协程借助udp发送向n发送findnode包
				go tab.findnode(n, targetKey, reply)
			}
		}
		if pendingQueries == 0 {
			// we have asked all closest nodes, stop the search
			break
		}
		select {
		case nodes := <-reply:
            // 遍历findnode查找到的结果放入result中,并将放入result中的每一个节点标记为seen
			for _, n := range nodes {
				if n != nil && !seen[n.ID()] {
					seen[n.ID()] = true
					result.push(n, bucketSize)
				}
			}
		case <-tab.closeReq:
			return nil // shutdown, no need to continue.
		}
        // pending计数递减
		pendingQueries--
	}
	return result.entries
}
// 下面是findnode的逻辑
func (tab *Table) findnode(n *node, targetKey encPubkey, reply chan<- []*node) {
	//先从数据库里面读出此节点的失败次数
	fails := tab.db.FindFails(n.ID(), n.IP())
	//使用udp 向n发送findnode包
	r, err := tab.net.findnode(n.ID(), n.addr(), targetKey)
	if err == errClosed {
		// Avoid recording failures on shutdown.
		reply <- nil
		return
	} else if len(r) == 0 {
        // 如果向n发送findnode包后返回的结果数为0,那么将此节点的查找失败次数加1 
		fails++
		tab.db.UpdateFindFails(n.ID(), n.IP(), fails)
		log.Trace("Findnode failed", "id", n.ID(), "failcount", fails, "err", err)
		//  如果让一个节点查找一个节点失败次数过多 比如大于5此  那么从本地buckets 里面删除此节点
		if fails >= maxFindnodeFailures {
			log.Trace("Too many findnode failures, dropping", "id", n.ID(), "failcount", fails)
			tab.delete(n)
		}
	} else if fails > 0 {
		//  如果本次查找成功 且此节点以前查找失败次数不为0  那么减少一次本节点的失败次数   防止好的节点被删除
		tab.db.UpdateFindFails(n.ID(), n.IP(), fails-1)
	}

	// Grab as many nodes as possible. Some of them might not be alive anymore, but we'll
	// just remove those again during revalidation.
	//将查找到的结果添加到自己的Bucket
	for _, n := range r {
		tab.addSeenNode(n)
	}
	//  将结果返回应答通道
	reply <- r
}
// 下面再分析两个方法findnode和addSeenNode
// findnode 发送一个findnode请求到给定的地址然后一直等待到节点发送k个邻居节点回来
// findnode sends a findnode request to the given node and waits until
// the node has sent up to k neighbors.
func (t *udp) findnode(toid enode.ID, toaddr *net.UDPAddr, target encPubkey) ([]*node, error) {
	// If we haven't seen a ping from the destination node for a while, it won't remember
	// our endpoint proof and reject findnode. Solicit a ping first.
	//24小时过期 计算上次收到to地址的ping包时间是否大于过期时间,如果大于  则ping对方  等待500ms 对方返回结果 ping对方后,对方会将本地节点加入对方的bucket里面
	if time.Since(t.db.LastPingReceived(toid, toaddr.IP)) > bondExpiration {
		t.ping(toid, toaddr)
		// Wait for them to ping back and process our pong.
		time.Sleep(respTimeout) //500毫秒
	}

	// Add a matcher for 'neighbours' replies to the pending reply queue. The matcher is
	// active until enough nodes have been received.
	nodes := make([]*node, 0, bucketSize)
	nreceived := 0
    // 下面将findnode的相关数据包装为一个replyMatcher放入plist等待回复
	errc := t.pending(toid, toaddr.IP, neighborsPacket, func(r interface{}) (matched bool, requestDone bool) {
		reply := r.(*neighbors)
		for _, rn := range reply.Nodes {
			nreceived++
			n, err := t.nodeFromRPC(toaddr, rn)
			if err != nil {
				log.Trace("Invalid neighbor node received", "ip", rn.IP, "addr", toaddr, "err", err)
				continue
			}
			nodes = append(nodes, n)
		}
		return true, nreceived >= bucketSize
	})
	t.send(toaddr, toid, findnodePacket, &findnode{
		Target:     target,
		Expiration: uint64(time.Now().Add(expiration).Unix()),
	})
    // 将findnode返回的结果放入nodes里面返回
	return nodes, <-errc
}
// 下面是将节点加入table下面bucket里面的逻辑
// addSeenNode adds a node which may or may not be live to the end of a bucket. If the
// bucket has space available, adding the node succeeds immediately. Otherwise, the node is
// added to the replacements list.
// 如果bucket[x]有位置则添加   否则 添加到replacement
// The caller must not hold tab.mutex.
func (tab *Table) addSeenNode(n *node) {
    // 若nodeId是本身则直接返回
	if n.ID() == tab.self().ID() {
		return
	}
	// 加锁
	tab.mutex.Lock()
	defer tab.mutex.Unlock()
	// 计算节点所处位置,若n和当前节点间的距离小于239(因为这样的节点很少)那么都放在buckets[0]位置
	b := tab.bucket(n.ID())
	//  判断是否已经包含
	if contains(b.entries, n.ID()) {
		// Already in bucket, don't add.
		return
	}
	// 判断桶里元素大小
	if len(b.entries) >= bucketSize {
		//如果通满了,则添加节点到替补位置
		// Bucket full, maybe add as replacement.
		tab.addReplacement(b, n)
		return
	}
	if !tab.addIP(b, n.IP()) {
		// Can't add: IP limit reached.
		return
	}
	// 如果桶没满,则追加到桶的最后一个位置
	// Add to end of bucket:
	b.entries = append(b.entries, n)
	//  并且尝试从replacements里面删除n
	b.replacements = deleteNode(b.replacements, n)
	//  将node节点添加进桶的时间更新为现在
	n.addedAt = time.Now()
	if tab.nodeAddedHook != nil {
		tab.nodeAddedHook(n)
	}
}

// addReplacement方法分析如下
func (tab *Table) addReplacement(b *bucket, n *node) {
	// 遍历所有替补  看是不是已经有节点n
	for _, e := range b.replacements {
		if e.ID() == n.ID() {
			return // already in list
		}
	}
	//
	if !tab.addIP(b, n.IP()) {
		return
	}
	var removed *node
	//开始将替补节点插入 替补buckets
	b.replacements, removed = pushNode(b.replacements, n, maxReplacements)
	if removed != nil {
		tab.removeIP(b, removed.IP())
	}
}
// pushNode adds n to the front of list, keeping at most max items.
func pushNode(list []*node, n *node, max int) ([]*node, *node) {
	//  首先判断 替补列表小于最大限制
	if len(list) < max {
		//那么给替补列表最后添加一个空元素
		list = append(list, nil)
	}
	// 将替补列表最后一个元素删除 上面和下面步骤合起来看   给列表添加一个空元素是为了返回值方便
	removed := list[len(list)-1]
	// 删除掉最后一个元素以后  将所有元素往后移动一位空出第一个位置  此时要删除的元素已经删除  存放在removed中
	copy(list[1:], list)
	//将第一个位置放入要添加的节点
	list[0] = n
	return list, removed
}

以上就是table下面doRefresh()相关逻辑,下面紧接着分析一下doRevalidate()和copyLiveNodes()的逻辑

// doRevalidate checks that the last node in a random bucket is still live
// and replaces or deletes the node if it isn't.
// 检查随机桶的最后一个节点是否存活,若不是则替代或者删除它
func (tab *Table) doRevalidate(done chan<- struct{}) {
	defer func() { done <- struct{}{} }()
	// 从随机桶buckets[random]中拿出最后一个节点,和桶索引
	last, bi := tab.nodeToRevalidate()
	if last == nil {
		// No non-empty bucket found.
		return
	}
	//对last节点进行一次ping操作
	// Ping the selected node and wait for a pong.
	err := tab.net.ping(last.ID(), last.addr())

	tab.mutex.Lock()
	defer tab.mutex.Unlock()
     // 获取bi位置的桶
	b := tab.buckets[bi]
	//如果ping通  则将bucket的最后一个元素移到最前面
	if err == nil {
         // 将last节点的判活检查计数自增,这个计数主要用于copyNodes
		// The node responded, move it to the front.
		last.livenessChecks++
		log.Debug("Revalidated node", "b", bi, "id", last.ID(), "checks", last.livenessChecks)
         // 将last移动到bucket[bi]的最前方
		tab.bumpInBucket(b, last)
		return
	}
	// 没有pong包收到时候,那么从replacement里面随机拿一个节点替换last ,如果buckets[bi]的替换者为空,那么将last直接删除
	// No reply received, pick a replacement or delete the node if there aren't
	// any replacements.
	if r := tab.replace(b, last); r != nil {
		log.Debug("Replaced dead node", "b", bi, "id", last.ID(), "ip", last.IP(), "checks", last.livenessChecks, "r", r.ID(), "rip", r.IP())
	} else {
		log.Debug("Removed dead node", "b", bi, "id", last.ID(), "ip", last.IP(), "checks", last.livenessChecks)
	}
}
//------------------------以上主要为revalidate的逻辑------------------------------------
//下面分析copyLiveNodes()逻辑
// copyLiveNodes adds nodes from the table to the database if they have been in the table
// longer then minTableTime.
//将盼活检查计数大于0的存入数据库
func (tab *Table) copyLiveNodes() {
	tab.mutex.Lock()
	defer tab.mutex.Unlock()

	now := time.Now()
    // 遍历table的所有bucket的所有节点
	for _, b := range &tab.buckets {
		for _, n := range b.entries {
			//  将判断盼活检查次数大于0且存在桶里的时间大于5分钟的节点写入数据库(目录为nodes)
			if n.livenessChecks > 0 && now.Sub(n.addedAt) >= seedMinTableTime {
				tab.db.UpdateNode(unwrapNode(n))
			}
		}
	}
}
2、总结

以上就是udp和table的主要逻辑分析,主要是以太坊节点发现机制的实现,结合p2p第一部分代码的分析,就可以很好的了解以太坊p2p底层,上层逻辑等待下次分享protocolManager逻辑时候就可以将p2p部分与以太坊上层应用如交易广播等等逻辑联系起来。

发布了12 篇原创文章 · 获赞 7 · 访问量 792

猜你喜欢

转载自blog.csdn.net/fh667788123/article/details/104960752