以太坊日志的bloom过滤器

最近研究以太坊的收据(Receipt),发现区块头中有一个Bloom字段,这个字段是个2048的字节数组:

const (
    // BloomByteLength represents the number of bytes used in a header log bloom.
    BloomByteLength = 256

    // BloomBitLength represents the number of bits used in a header log bloom.
    BloomBitLength = 8 * BloomByteLength
)

// Bloom represents a 2048 bit bloom filter.
type Bloom [BloomByteLength]byte

通过注释知道这是一个Bloom过滤器,但是这个过滤器是如何工作的。引起了我的兴趣,这一看相关代码,让我预闷了一周多的时间。各种通道和文件让我没有任何头绪来了解这个Bloom是如何起到过滤作用的。现在有了点头绪,想通过几篇文章做个分享。

一。Bloom是如何生成的

~~~
b.header.Bloom = CreateBloom(receipts)
~~~

func CreateBloom(receipts Receipts) Bloom {
    bin := new(big.Int)
    for _, receipt := range receipts {
        bin.Or(bin, LogsBloom(receipt.Logs))
    }

    return BytesToBloom(bin.Bytes())
}

func LogsBloom(logs []*Log) *big.Int {
    bin := new(big.Int)
    for _, log := range logs {
        bin.Or(bin, bloom9(log.Address.Bytes()))
        for _, b := range log.Topics {
            bin.Or(bin, bloom9(b[:]))
        }
    }

    return bin
}

func bloom9(b []byte) *big.Int {
    b = crypto.Keccak256(b)

    r := new(big.Int)

    for i := 0; i < 6; i += 2 {
        t := big.NewInt(1)
        b := (uint(b[i+1]) + (uint(b[i]) << 8)) & 2047
        r.Or(r, t.Lsh(t, b))
    }

    return r
}

通过源码我们可以看到Bloom数组就是从收据(Receipt)中的Logs字段生成的。具体的内容是Logs中的Address和Topics字段,为了大家好理解,我简单介绍下Log中的Address和Topics字段所代表的内容:

type Log struct {
//以太坊智能合约的地址
Address common.Address json:"address" gencodec:"required"
// 以太坊智能合约转账过程中相关的转入转出方的地址hash
Topics []common.Hash json:"topics" gencodec:"required"
//以太坊智能合约转账的金额
Data []byte json:"data" gencodec:"required"

Address:代表合约,Topics代表地址,合起来就是某个合约Log的相关地址
为了大家了解就先这样了解上面的字段。如果想更深入,请查看其他文档。

所以看到这里我们就明白了区块头的Bloom字段其实就是根据一套算法(bloom9)将合约Address和Topics生成了一个2048字节的数组。

二。Bloom9算法

我们已经知道布隆过滤器里存储的是Log的合约地址(Address)和相关账户哈希(Topics)。布隆过滤器的一个特性就是不保证一个内容100%存在,但可以100%保证一个内容不存在过滤器中。了解了这个特性我们就介绍下布隆过滤器的生成算法Bloom9:

1.首先将传入的任何数据,进行Keccak256的运算,得到一个32字节的hash 。
2.循环取此hash值的前6字节,注意循环步进是i+=2,意思是每两个字节合为一个uint,并和2047进行位与运算(其实就是和2048求余),得到一个小于2048的值b,这个值b就是一个索引(index),表示在Bloom过滤器中的位置下标,也就是Bloom[b]的值为1。所以前6个字节生成了三个下标,共同表示此值是否在Bloom过滤器中。所以如果它生成的三个下标都不都为1。就能100%保证此值不在过滤器中,如果都为1,也不一定保证在这个过滤器中,但我们就认为在了。这就是过滤器的特性.
3.将生成的三个下标值进行左位移和位或运算,将相应的下标值值为1,,得到的这个bigInt就是传入参数的bloom过滤器的索引(index)值.

三。LogsBloom算法

这个方法就很简单了,该方法就是把Log的合约地址(Address)和Topics分别转为相应的下标值(bloom9值),然后分别将相应的下标值进行位或操作。这样就将他们的值都存在了一个数据里面。最后在函数CreateBloom()中将这个数据转为了2048位的Bloom类型的值。
所以此Bloom过滤器只是一个相应数据的标志器,用于判断某个数据是否存在里面的工具,本身不包括任何数据

四。如何判断一个数据(BloomLookup)

我们知道了Bloom中的内容并不包含相应的数据,所以判断一个数据的存在不存在,并不是查找相应的数据,而且将相应的数据索引化(bloom9)后的下标值在Bloom中的位置的值是否全为1。但以太坊的BloomLookup提供了另一个思路:

func BloomLookup(bin Bloom, topic bytesBacked) bool {
    bloom := bin.Big()
    cmp := bloom9(topic.Bytes())

    return bloom.And(bloom, cmp).Cmp(cmp) == 0
}

通过源码可以看到其判断流程为:

1.先将传入的数据转成bloom9值cmp
2.将Bloom过滤器转为Big.Int然后和cmp进行位与运算
3.将位与后的值和cmp比较是否相等,如果相等表示存在
func bloomFilter(bloom types.Bloom, addresses []common.Address, topics [][]common.Hash) bool {
    if len(addresses) > 0 {
        var included bool
        for _, addr := range addresses {
            if types.BloomLookup(bloom, addr) {
                included = true
                break
            }
        }
        if !included {
            return false
        }
    }

    for _, sub := range topics {
        included := len(sub) == 0 // empty rule set == wildcard
        for _, topic := range sub {
            if types.BloomLookup(bloom, topic) {
                included = true
                break
            }
        }
        if !included {
            return false
        }
    }
    return true
}

这个bloomFilter方法就是一个使用示例,这个函数的功能就是:

1.判断是否存在某些合约(addresses),如果不存在,直接返回false,就不用判断topics了。因为topics就是某个合约的转账过程的账户地址,没有合约,当然也就没有topics了
2.如果存在某些合约,就查找是否存在某些账户(topic)。

这就是布隆过滤器的使用方式和原理了 。

OK,这篇我们就先介绍到这里,让大家了解以太坊中区块头的Bloom字段的作用就是根据合约地址和账户地址过滤以太坊日志的功能。方便用户可以查看自己感兴趣的日志。下面我们会逐步将这些内容铺开来介绍。

你可能感兴趣的:(以太坊日志的bloom过滤器)