以太坊细节[2]:msg到达时间的来源
2021-05-29

背景

有些依赖时间弱同步假设的共识,比如PBFT,DPOS,和一些元胞自动机衍生(MoCA)的共识。在实现的时候需要得到消息的到达时间。
学习了一下以太坊如何处理区块到达时间的。这里记录一下。

消息中ReceivedAt的写入逻辑

一切的起点是p2p模块的server.run()函数
从这里开始,geth启动了p2p服务器。

func (srv *Server) run(dialstate dialer) {
	srv.log.Info("Started P2P networking", "self", srv.localnode.Node().URLv4())
	...
				go srv.runPeer(p)
				peers[c.node.ID()] = p
    ...
}

调用runPeer函数 p2p/server.go, L1044 - L1067

func (srv *Server) runPeer(p *Peer) {
	...
	remoteRequested, err := p.run()
    ...
}

p2p服务器通过runPeer函数,调用了peerrun函数
启动节点。
run函数中启动了各种监听,

func (p *Peer) run() (remoteRequested bool, err error) {
	...
	go p.readLoop(readErr)
	go p.pingLoop()
    ...
}

跟进readLoop中就可以看到对msg的操作。

func (p *Peer) readLoop(errc chan<- error) {
	defer p.wg.Done()
	for {
		msg, err := p.rw.ReadMsg()
		if err != nil {
			errc <- err
			return
		}
		msg.ReceivedAt = time.Now()
		if err = p.handle(msg); err != nil {
			errc <- err
			return
		}
	}
}

可见 readLoop函数会循环调用rw中的ReadMsg
向读出的msg中写入msg.ReceivedAt值为当前时间
这里的ReceivedAt就可以在后面用来判断消息的到达时间,通过比较ReceivedAtblock.header.timestamp就可以很方便的判断消息在网络中游荡的时间长度。
函数readLoop中,将处理好的msg送到了p.handle
peer.handle中,又将msg给到了proto.in

func (p *Peer) handle(msg Msg) error {
    ...
    proto, err := p.getProto(msg.Code)
    ...
		select {
		case proto.in <- msg:
			return nil
	...
}

进入getProto函数中,看一看这个proto是啥。

func (p *Peer) getProto(code uint64) (*protoRW, error) {
	for _, proto := range p.running {
		if code >= proto.offset && code < proto.offset+proto.Length {
			return proto, nil
		}
	}
    ...
}

这里可以看到所有proto来自p.running域,
Peer的结构定义中,我们可以找到running的数据类型

type Peer struct {
    ...
	running map[string]*protoRW
    ...
}

running是一组protoRW
protoRW.in被赋值时,protoRW.ReadMsg函数就不再堵塞。

func (rw *protoRW) ReadMsg() (Msg, error) {
	select {
	case msg := <-rw.in:
		msg.Code -= rw.offset
		return msg, nil
	case <-rw.closed:
		return Msg{}, io.EOF
	}
}

这样就又回到了readLoop函数的开始。
可见readLoop函数的作用,就是为所有msg加上ReceivedAt
重新再放回到ReadMsg中.

区块中ReceivedAt的赋值。

知道了ReceivedAt是怎么来的。现在看一下在哪里使用了它。
以太坊的用法如下
首先由ProtocolManager调用handle开启监听。

func (pm *ProtocolManager) handle(p *peer) error {
	...
		if err := pm.handleMsg(p); err != nil 
    ...
}

ProtocolManager.handle中调用消息监听pm.handleMsg函数。

func (pm *ProtocolManager) handleMsg(p *peer) error {
	// Read the next message from the remote peer, and ensure it's fully consumed
	msg, err := p.rw.ReadMsg()
	...
	case msg.Code == NewBlockMsg:
	...
		request.Block.ReceivedAt = msg.ReceivedAt
		request.Block.ReceivedFrom = p
	...

可见当收到NewBlockMsg消息的时候,
解析出的block会做block.ReceivedAt = msg.ReceivedAt操作
至此,区块block就知道自己是什么时候到的该节点了。

总结

如果是从Block.ReceivedAt一点一点往回追,
会卡在ProtocolManagerp.rw.ReadMsg函数上。
ProtocolManger
这个函数本身没有对ReceivedAt的定义。
而且其中的in信道也很难find。
不过,换个角度。从p2p.sevrer开始追。
一直追到readLoop
就恍然大悟了。

以太坊的消息到达时间处理流程如下。
以太坊ReceivedAt处理流程