Skip to content

Latest commit

 

History

History
147 lines (116 loc) · 8.58 KB

File metadata and controls

147 lines (116 loc) · 8.58 KB

Whisper网络

采用Kad算法构建的p2p网络不仅可以提供给区块链应用使用,也可以提供给其他的分布式应用使用,以太坊2.0就计划使用libp2p库构建kad网络,同时libp2p也是分布式文件系统ipfs所使用的底层网络库。我们也可以基于这个kad网络构建自己的区块链系统,这就好比电脑连接一条网线以后可以听歌,打游戏,浏览网页,而kad网络就是这根网线。

以采用Kad算法构建的网络作为基础,可以在其之上创建不同的子协议来支持不同的应用。

以太坊的P2P网络也确实是这样做的,如果将以太坊的P2P类比做tcp协议,那么p2p暴露出来的子协议就类似HTTP。以太坊在这种支持子协议的P2P网络基础上构建出了自己的whisper网络用以支持以太坊运行。

在以太坊启动子协议前,需要对整个网络总体进行配置;

srv := p2p.Server{
    Config: p2p.Config{
        MaxPeers: 10,
        PrivateKey: nodekey,
        Name: "NodeName",
        ListenAddr: ":30300",
        Protocols: []p2p.Protocol{},
        NAT: nat.Any(), // 支持内网穿透
        Logger: log.New(),
    },
}

在启动子协议前,首先需要对网络层的Server进行配置,其中包括最多可以连接的节点数量,本节点的私钥等,等配置完成之后就可以建立节点间的连接了。

以太坊中,所有的RLPx子协议都是基于这条连接的,整体关系如下图;

当需要自定义子协议的时候,只需要按照以太坊子协议规范定义一个一个结构体即可;

func MyProtocol() p2p.Protocol {
  return p2p.Protocol{ // 1.
    Name: "MyProtocol", // 2.
    Version: 1, // 3.
    Length: 1, // 4.
    Run: func(peer *p2p.Peer, ws p2p.MsgReadWriter) error { return nil }, // 5.
  }
}
  • Name:一个子协议即一个p2p.Protocol;
  • Version:子协议名,需要唯一标识该子协议版本号,当一个子协议有多个版本时,采纳最高版本的协议;
  • Length:该协议拥有的消息类型个数,p2p的handler需要知道应该预留多少空间以用来服务你的协议。这是也是共识信息能够通过message ID到达各个peer并实现协商的保障。
  • Run:当在以太坊p2p的server启动的时候会调用这个方法来启动子协议,子协议启动以后需要这个方法来处理消息。

其中的Length表示一个子协议有多少消息,结合状态码来分发消息,比如子协议1的长度是13,表示有13个消息,其消息中可以使用的code就是0-12,当p2p server收到一个消息时,通过查看code就可以将消息分发给对应的子协议来处理,多子协议如下图所示;

这样就实现了在一条TCP连接上支持多种协议,避免了为每个子协议建立多个连接所带来的消耗。在以太坊中,所有数据在传输前都需要经过RLP编码以减少消息体积,这也是RLPx协议名称的由来。

消息广播

随着子协议的增加,这个子网络中的消息也越来越多,但是一部分节点只关注特定的消息,其余的消息对它来说只能是负担。whisper协议对上层暴露出一套类似于订阅-发布的API模型,节点可以申请自己感兴趣的topic,那么就只会接收到这些topic的消息,无关主题的消息将被丢弃。在这套体系内,有几个基础构件需要说明下:

Envelope信封

envelope即信封是whisper网络节点传输数据的基本形式。信封包含了加密的数据体和明文的元数据,元数据主要用于基本的消息校验和消息体的解密。

信封是以RLP编码的格式传输[ Version, Expiry, TTL, Topic, AESNonce, Data, EnvNonce ]

  • Version:最多4字节(目前仅使用了1字节),如果信封的Version比本节点当前值高,将无法解密,仅做转发
  • Expiry:4字节(unix时间戳秒数),过期时间
  • TTL:4字节,剩余存活时间秒数
  • Topic:4字节,信封主题
  • AESNonce:12字节随机数据,仅在对称加密时有效
  • Data:消息体
  • EnvNonce:8字节任意数据(用于PoW计算)

如果节点无法解密信封,那么节点对信封内的消息内容一无所知,但这并不影响节点将消息进行转发扩散。当发送一个消息的时候需要进行PoW工作量证明,节点要做一些计算,消耗一定的时间和计算资源,避免一个节点无限制的发送垃圾消息,降低网络负担。计算PoW所付出的代价可以理解为抵扣节点为传播和存储信息所花费的资源。

在whisperv5中,PoW定义为:

PoW = (2^BestBit) / (size * TTL)
  • BestBit是hash计算值的前导0个数
  • size是消息大小
  • TTL剩余存活时间秒数

whisper节点发送消息需要经过创建消息whisper.NewSentMessage()—->封装入信封msg.Wrap(msg)—->发送消息shh.Send(),消息的工作量证明就在第二步装入信封的时候进行计算。

Warp函数最终调用Seal:

github.com/ethereum/go-ethereum/whisper/whisperv5/envelope.go

func (e *Envelope) Seal(options *MessageParams) error {
    var target, bestBit int // target是需要达到的目标前置0位数
    if options.PoW == 0 {
        // 将消息过期时间调整到工作量计算完成后
        e.Expiry += options.WorkTime
    } else {
        // 根据公式 PoW = (2^BestBit) / (size * TTL) 从预设的PoW阈值反解出BestBit
        target = e.powToFirstBit(options.PoW)
        if target < 1 {
            target = 1
        }
    }

    buf := make([]byte, 64)
    // Keccak256是SHA-3的一种,Keccak已可以抵御最小的复杂度为2n的攻击,其中N为散列的大小。它具有广泛的安全边际。至目前为止,第三方密码分析已经显示出Keccak没有严重的弱点
    h := crypto.Keccak256(e.rlpWithoutNonce())
    copy(buf[:32], h)

    finish := time.Now().Add(time.Duration(options.WorkTime) * time.Second).UnixNano()
    for nonce := uint64(0); time.Now().UnixNano() < finish; {
        for i := 0; i < 1024; i++ {
            // 暴力尝试nonce值
            binary.BigEndian.PutUint64(buf[56:], nonce)
            d := new(big.Int).SetBytes(crypto.Keccak256(buf))
            firstBit := math.FirstBitSet(d)
            if firstBit > bestBit {
                e.EnvNonce, bestBit = nonce, firstBit
                // 当尝试得到满足条件的EnvNonce,计算完成
                if target > 0 && bestBit >= target {
                    return nil
                }
            }
            nonce++
        }
    }
    if target > 0 && bestBit < target {
        return fmt.Errorf("failed to reach the PoW target, specified pow time (%d seconds) was insufficient", options.WorkTime)
    }
    return nil
}

通信流程

whisper协议的实现位于包github.com/ethereum/go-ethereum/whisper,该包下面有多个版本实现,目前最新协议包是whisperv6.

whisper节点启动后产生两个分支:

一个分支负责清理shh.envelopes中的过期消息 另一个分支(proccessQueue)从两个队列取出新接收到的消息,根据消息对应topic投放(Trigger)到对应接收者(filter),从而交付给上层应用进行处理 补充说下whisper里两个队列messageQueue,p2pMsgQueue的不同作用,messageQueue接收普通的广播消息,p2pMsgQueue接收点对点的直接消息,可绕过pow和ttl限制。

whisper 协议

whisper协议的具体实现里,代码流程也非常清晰:

每个peer连接成功后,产生两个goroutine,进行消息接收和广播:

  • 接收消息协程不断从连接中读取新消息,并且将消息暂存到shh.envelopes中,如果发现是一条未接收过的新消息,则将消息转发到对应的队列(messageQueue,p2pMsgQueue)
  • 广播协程负责将该peer未接收过的消息(本节点认为该peer未接收过,并非peer一定没接收过,p2p网络其他节点可能已经将消息广播到该节点了)投递到该节点

经过一系列精心设计后,我们的网络架构可以连接大量的节点,支持多种子协议运行,具有一定的鲁棒性,即可全网广播消息,又可以单点通信,可以说是一种非常完善的网络架构,足以支持我们在这个网络上构建可靠的区块链系统。