深入ethereum源码-whisper协议解读

whisper协议是以太坊DApps之间的通信协议。

概述

whisper是完全基于ID的消息系统,它的设计目的是形成一套p2p节点间的异步广播系统。whisper网络上的消息是加密传送的,完全可以暴露在公网进行传输;此外,为了防范DDos攻击,whisper使用了proof-of-work(PoW)工作量证明提高消息发送门槛。

whisper基础构件

whisper协议对上层暴露出一套类似于订阅-发布的API模型,节点可以申请自己感兴趣的topic,那么就只会接收到这些topic的消息,无关主题的消息将被丢弃。在这套体系内,有几个基础构件需要说明下:

Envelope信封

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

信封是以RLP编码的格式传输:

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

如果节点无法解密信封,那么节点对信封内的消息内容一无所知,单这并不影响节点将消息进行转发扩散。

Message消息

信封内的消息体解密后即得到消息内容。

Topic主题

每个信封上都有一个主题,注意主题可以仅使用部分前缀

Filter过滤器

filter订阅-发布模型中的订阅者

PoW工作量证明

PoW的存在是为了反垃圾信息以及降低网络负担。计算PoW所付出的代价可以理解为抵扣节点为传播和存储信息锁花费的资源.

whisperv5中,PoW定义为:

1
PoW = (2^BestBit) / (size * TTL)
  • BestBit是hash计算值的前导0个数
  • size是消息大小
  • TTL

具有高PoW的消息具有优先处理权。

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

Warp函数最终调用Seal:

github.com/ethereum/go-ethereum/whisper/whisperv5/envelope.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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 main loop

whisper-main-loop

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

  • 一个分支负责清理shh.envelopes中的过期消息
  • 另一个分支(proccessQueue)从两个队列取出新接收到的消息,根据消息对应topic投放(Trigger)到对应接收者(filter),从而交付给上层应用进行处理

补充说下whisper里两个队列messageQueue,p2pMsgQueue的不同作用,messageQueue接收普通的广播消息,p2pMsgQueue接收点对点的直接消息,可绕过powttl限制.

whisper protocol

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

whisper-peer-loop

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

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

Comments