在区块链技术中,如何高效、快速地在不下载完整数据的情况下,判断某个交易是否存在于特定的区块中,或者某个合约是否发出了特定的日志事件,是一个核心问题,以太坊为了解决这个问题,巧妙地运用了一种概率性数据结构——Bloom过滤器,本文将深入以太坊的源码,探讨其Bloom过滤器的实现原理、编码细节及其在以太坊网络中的关键作用。
什么是Bloom过滤器
Bloom过滤器是由Burton Howard Bloom于1970年提出的一种空间效率很高的随机数据结构,它利用位数组来表示一个集合,并用于判断一个元素是否可能属于该集合,或一定不属于该集合。
其主要特点:
- 空间效率高:相比于存储元素本身,Bloom过滤器只需要很小的存储空间。
- 查询速度快:时间复杂度为O(k),k为哈希函数的数量。
- 概率性:
- 假阳性(False Positive):可能会判断一个元素在集合中,即使它并不在,这是Bloom过滤器最主要的特性。
- 假阴性(False Negative):绝对不会发生,如果判断元素不在集合中,那么它一定不在。
- 确定性删除困难:标准Bloom过滤器不支持元素的删除(除非使用Counting Bloom Filter等变种)。
在以太坊中,每个区块头都包含一个bloom字段,这是一个由该区块内所有交易产生的日志事件共同构建的Bloom过滤器,节点可以通过查询这个过滤器,快速判断某个地址或主题是否在该区块中有日志,从而决定是否需要下载该区块的完整日志数据。
以太坊Bloom过滤器的设计与源码结构
以太坊的Bloom过滤器实现主要位于其核心代码库的eth或core包中(具体路径可能因版本和客户端实现而异,如Go-Ethereum的core/types包),我们以Go-Ethereum(geth)为例,来剖析其源码。
数据结构定义
在core/types/block.go或相关文件中,我们可以找到Bloom类型的定义:
// Bloom represents a 2048 bit bloom filter. type Bloom [256]byte // 256 bytes = 2048 bits
这是一个长度为256字节的数组,总共2048位,这就是Bloom过滤器的位数组。
Bloom过滤器构建(添加元素)
构建Bloom过滤器的过程,就是将多个元素(在以太坊中主要是日志主题和地址)通过多个哈希函数映射到位数组中的特定位置,并将这些位置的位设置为1。
以太坊使用了3个不同的哈希函数(实际上是SHA3哈希的不同部分)来确保均匀分布和降低假阳性率,这个逻辑通常在一个名为AddBloom的函数中实现(可能在core/bloom或直接在types包内)。
核心步骤如下(伪代码/关键逻辑):
func AddBloom(b *Bloom, data []byte) {
// 使用3个不同的哈希函数对data进行哈希
// 在以太坊中,这通常是通过截取SHA3哈希的不同部分来模拟的
hash1 := sha3.Sum256(data) // 第一个哈希值
hash2 := sha3.Sum256(append(data, 0x01)) // 第二个哈希值(通过添加不同后缀区分)
hash3 := sha3.Sum256(append(data, 0x02)) // 第三个哈希值
// 从每个哈希值中提取足够的位来定位到2048位中的某一位
// 通常取哈希的低几位进行组合,确保索引在0-2047之间
//
idx1 := binary.BigEndian.Uint64(hash1[:8]) % 2048
idx2 := binary.BigEndian.Uint64(hash2[:8]) % 2048
idx3 := binary.BigEndian.Uint64(hash3[:8]) % 2048
// 将位数组中对应的位置为1
setBit(b, idx1)
setBit(b, idx2)
setBit(b, idx3)
}
// setBit 将Bloom过滤器指定位设置为1
func setBit(b *Bloom, idx uint64) {
bytePos := idx / 8
bitPos := idx % 8
b[bytePos] |= 1 << (7 - bitPos) // 大端序,高位在前
}
在以太坊的实际源码中,CreateBloom函数会遍历区块中的所有交易,再遍历每个交易中的所有日志,然后对每个日志的地址(Address)和每个主题(Topic)调用类似的AddBloom逻辑,最终将所有这些哈希结果合并到一个大的Bloom过滤器中。
Bloom过滤器查询(检查元素)
查询一个元素是否可能在Bloom过滤器中,过程与添加类似:
- 对该元素应用相同的3个哈希函数,得到3个索引值。
- 检查这3个索引值在位数组中对应的位是否都为1。
- 如果有任何一位为0,则该元素一定不在集合中(假阴性不可能)。
- 如果所有位都为1,则该元素可能在集合中(存在假阳性的可能)。
以太坊源码中对应的查询函数类似:
func BloomLookup(b Bloom, data []byte) bool {
hash1 := sha3.Sum256(data)
hash2 := sha3.Sum256(append(data, 0x01))
hash3 := sha3.Sum256(append(data, 0x02))
idx1 := binary.BigEndian.Uint64(hash1[:8]) % 2048
idx2 := binary.BigEndian.Uint64(hash2[:8]) % 2048
idx3 := binary.BigEndian.Uint64(hash3[:8]) % 2048
return getBit(b, idx1) && getBit(b, idx2) && getBit(b, idx3)
}
// getBit 获取Bloom过滤器指定位的值
func getBit(b Bloom, idx uint64) bool {
bytePos := idx / 8
bitPos := idx % 8
return (b[bytePos] >> (7 - bitPos)) & 1 == 1
}
特殊的“OR”操作
由于一个区块的Bloom过滤器是由该区块内所有交易的日志Bloom过滤器“或”(OR)运算得到的,以太坊源码中会有一个BloomOr函数,用于将两个Bloom过滤器按位进行OR操作,合并结果。
func BloomOr(b1, b2 Bloom) Bloom {
var result Bloom
for i := 0; i < 256; i++ {
result[i] = b1[i] | b2[i]
}
return result
}
这个特性使得构建大型区块的Bloom过滤器变得高效,可以并行处理各个交易的日志Bloom,最后再合并。
源码中的关键细节与优化
- 哈希函数的选择:以太坊使用SHA3(Keccak)哈希算法的变体来生成哈希值,通过使用不同的输入(如添加后缀

0x01,0x02)来模拟多个哈希函数,既保证了哈希的质量,又避免了实现多个独立哈希函数的复杂性。 - 位序与字节序:在设置和获取位时,需要注意大端序(BigEndian)的影响,确保位的位置计算正确,以太坊的实现中,每个字节的最高位(MSB)对应索引的较低位(字节0的bit7对应索引0,bit6对应索引1,.,bit0对应索引7)。
- 日志数据的编码:在将地址(Address)和主题(Topic)添加到Bloom过滤器之前,它们会被序列化为特定的格式,地址通常是20字节,主题通常是32字节(对于主题0)或未定义(对于其他主题,会被视为空)。
- 区块头中的Bloom字段:区块头中的
Bloom [256]byte字段就是由上述过程生成的,它包含了该区块所有日志信息的“指纹”。
Bloom过滤器在以太坊中的应用场景
- 轻客户端(Light Clients):轻客户端无法存储完整的区块链数据,它们可以下载区块头,并通过查询区块头的Bloom字段,快速判断某个地址的活动或某个主题的事件是否发生在某个区块中,从而决定是否需要从全节点获取该区块的完整日志或交易数据。
- 事件索引与查询:DApp开发者可以通过以太坊的JSON-RPC接口(如
eth_getLogs)来查询符合特定条件的日志,节点在处理这类查询时,首先会使用Bloom过滤器进行初步筛选,只处理那些可能包含目标日志的区块,大大提高了查询效率,尤其是在处理大量历史数据时。 3