币灵灵财经
首页 > 币圈新闻 > 文章正文

BIP 158 致密区块过滤器详解

币灵灵财经 2024-10-24 09:44 505

欧易交易所

欧易交易所

软件大小:268.26MB

软件版本:v3.4.2

BIP 158 致密区块过滤器详解

区块过滤器的意义

布隆过滤器有什么问题?

问题在于,这个过程并不是非常私密,因为,你还是向这个接收过滤器的比特币节点泄露了一些信息。他们可以知晓你感兴趣的交易以及你完全没兴趣的交易;他们还可以不给你发送跟过滤器相匹配的东西。所以,这对轻客户端来说并不是理想的。但同时,它对向轻客户端提供服务的比特币节点来说也不理想。每次你发送一个过滤器,他们就必须从硬盘加载相关的区块,然后确定是否有交易跟你的过滤器相匹配。只需要不停发送假的过滤器,你就可以轰炸他们 —— 这本质上是一种 DOS 攻击。只需要非常少的能量就可以创建一个过滤器,但需要耗费很多能量,才能响应这样的请求。

致密区块过滤器

OK,那么我们需要的属性有:

objects = {spk1, spk2, spk3, spk4, ..., spkN} // 一个由 N 个脚本公钥构成的列表

首先,我们可以将每一个对象都转化成某个范围内的一个数字、并使得每个对象数字在这个范围内是均匀分布的。假设我们有 10 个对象(N = 10),然后我们有某一种函数,可以将每个对象都转化成一个数字。假设我们选择的范围是 [0, 10](因为我们有 10 个对象)。现在,我们的 “哈希加转换成数字” 函数将取每一个对象为输入,并分别产生一个大小在 [0, 10] 之间的数字。这些数字在这个范围内是均匀分布的。这就意味着,在排序之后,我们将得到一个这样的图(在非常非常理想的情况下):

BIP 158 致密区块过滤器详解

首先,这非常棒,因为我们已经大大缩减了每一个对象的指纹的大小。现在每个对象都只是一个数字了。那么,我们的新过滤器看起来就像这样:

numbers := {1,2,3,4,5,6,7,8,9,10} BIP 158 致密区块过滤器详解 BIP 158 致密区块过滤器详解 BIP 158 致密区块过滤器详解 0000 10000 10000 10000 10000 10000 10000 10000 10000 10000 [(0, 0), (1, 0), (1, 0), (1, 0), (1, 0), (1, 0), (1, 0), (1, 0), (1, 0), (1, 0)] [0, 5, 5, 5, 5, 5, 5, 5, 5, 5] [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]

更加现实的例子

$ bitcoin-cli getblockhash 2101914000000000000002c06f9afaf2b2b066d4f814ff60cfbc4df55840975a00e035c$ bitcoin-cli getblockfilter 000000000000002c06f9afaf2b2b066d4f814ff60cfbc4df55840975a00e035c{  "filter": "5571d126b85aa79c9de56d55995aa292de0484b830680a735793a8c2260113148421279906f800c3b8c94ff37681fb1fd230482518c52df57437864023833f2f801639692646ddcd7976ae4f2e2a1ef58c79b3aed6a705415255e362581692831374a5e5e70d5501cdc0a52095206a15cd2eb98ac980c22466e6945a65a5b0b0c5b32aa1e0cda2545da2c4345e049b614fcad80b9dc9c903788163822f4361bbb8755b79c276b1cf7952148de1e5ee0a92f6d70c4f522aa6877558f62b34b56ade12fa2e61023abf3e570937bf379722bc1b0dc06ffa1c5835bb651b9346a270",  "header": "8d0cd8353342930209ac7027be150e679bbc7c65cc62bb8392968e43a6ea3bfe"} func constructFilter(bc *`bitcoind`.Bitcoind, block `bitcoind`.Block) ([]byte, error) {    // 1. 从区块中收集我们想要添加到一个过滤器中的所有对象    // 2. 将这些对象转化为数字,并排序     // 3. 取得排序后的数字列表中相邻两数的差值    // 4. 使用 Golomb-Rice 编码来编码这些差值} // 我们希望向过滤器添加的对象的列表// 包括所有被花费的脚本公钥,以及每一个输出的脚本公钥// 我们使用了 “图(map)”,这样就去除了重复的脚本公钥objects := make(map[string] struct{})// 便利区块中的每一笔交易for i, tx := range block.Tx {        // 添加每一个输出的脚本公钥        // 到我们的对象列表中        for _, txOut := range tx.Vout {                scriptPubKey := txOut.ScriptPubKey                if len(scriptPubKey) == 0 {                        continue                }                // 如遇 OP_RETURN (0x6a) 输出,则跳过                if spk[0] == 0x6a {                        continue                }                objects[skpStr] = struct{}{}        }        // 不添加 coinbase 交易的输入        if i == 0 {                continue        }        // 对每一个输入,获取其脚本公钥        for _, txIn := range tx.Vin {                prevTx, err := bc.GetRawTransaction(txIn.Txid)                if err != nil {                        return nil, err                }                scriptPubKey := prevTx.Vout[txIn.Vout].ScriptPubKey                if len(scriptPubKey) == 0 {                        continue                }                objects[spkStr] = struct{}{}        }}

OK,现在已经收集好了所有的对象。现在,我们定义变量 N 为对象图的长度。在这里,N 为 85 .

numbers := make([]uint64, 0, N)// 遍历所有对象,将它们转化为在 [0, F] 范围内均匀分布的数字// 并将这些数字存储为 `numbers` 列表for o := range objects {        // Using the given key, max number (F) and object bytes (o),        // convert the object to a number between 0 and F.        v := convertToNumber(b, F, key)        numbers = append(numbers, v)}// 给 numbers 列表排序sort.Slice(numbers, func(i, j int) bool { return numbers[i] < numbers[j] })// 将数字列表转化为差值列表differences := make([]uint64, N)for i, num := range numbers {        if i == 0 {                differences[i] = num                continue        }        differences[i] = num - numbers[i-1]} BIP 158 致密区块过滤器详解 filter := bstream.NewBStreamWriter(0)// 对于 `differences` 列表中的每个数值,通过除以 2 ^ P 来获得商和余数。for _, d := range differences {        q := math.Floor(float64(d)/math.Exp2(float64(P)))        r := d - uint64(math.Exp2(float64(P))*q)        // 编码商        for i := 0; i < int(q); i++ {               filter.WriteBit(true)        }        filter.WriteBit(false)        filter.WriteBits(r, P)} 71d126b85aa79c9de56d55995aa292de0484b830680a735793a8c2260113148421279906f800c3b8c94ff37681fb1fd230482518c52df57437864023833f2f801639692646ddcd7976ae4f2e2a1ef58c79b3aed6a705415255e362581692831374a5e5e70d5501cdc0a52095206a15cd2eb98ac980c22466e6945a65a5b0b0c5b32aa1e0cda2545da2c4345e049b614fcad80b9dc9c903788163822f4361bbb8755b79c276b1cf7952148de1e5ee0a92f6d70c4f522aa6877558f62b34b56ade12fa2e61023abf3e570937bf379722bc1b0dc06ffa1c5835bb651b9346a270

除了开头的两个字节,其余都跟我们在 bitcoind 中得到的过滤器完全一样!为什么前面的 2 字节会有区别呢?因为 BIP 说 N 的值需要在 CompactSize 格式中编码,并出现在过滤器的前面,这样才能被接收者解码。这是用下列办法完成的:

fd := filter.Bytes()var buffer bytes.Bufferbuffer.Grow(wire.VarIntSerializeSize(uint64(N)) + len(fd))err = wire.WriteVarInt(&buffer, 0, uint64(N))if err != nil {        return nil, err}_, err = buffer.Write(fd)if err != nil {        return nil, err} 5571d126b85aa79c9de56d55995aa292de0484b830680a735793a8c2260113148421279906f800c3b8c94ff37681fb1fd230482518c52df57437864023833f2f801639692646ddcd7976ae4f2e2a1ef58c79b3aed6a705415255e362581692831374a5e5e70d5501cdc0a52095206a15cd2eb98ac980c22466e6945a65a5b0b0c5b32aa1e0cda2545da2c4345e049b614fcad80b9dc9c903788163822f4361bbb8755b79c276b1cf7952148de1e5ee0a92f6d70c4f522aa6877558f62b34b56ade12fa2e61023abf3e570937bf379722bc1b0dc06ffa1c5835bb651b9346a270 b := bstream.NewBStreamReader(filter)var (    numbers []uint64    prevNum uint64)for {        // 从字节串中读取商。遇到的第一个 0 表示商的结尾        // 再遇到 0 之前,1 的数量就代表了商        var q uint64        c, err := b.ReadBit()        if err != nil {                return err        }        for c {                q++                c, err = b.ReadBit()                if errors.Is(err, io.EOF) {                        break                } else if err != nil {                        return err                }        }        // 随后的 P 个比特编码了余数        r, err := b.ReadBits(P)        if errors.Is(err, io.EOF) {                break        } else if err != nil {                return err        }        n := q*uint64(math.Exp2(float64(P))) + r        num :=  n + prevNum        numbers = append(numbers, num)        prevNum = num}fmt.Println(numbers)