Merkle树的生成和SPV验证信息的策略

Posted white and white

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Merkle树的生成和SPV验证信息的策略相关的知识,希望对你有一定的参考价值。

最近项目需要用到区块链来对某些信息进行快速校验,且由于每天的信息量都是很大的,因此直接使用区块链来保存信息是不可行的,故使用Merkle树的叶子节点先对传入的信息进行一次hash加密,紧接着利用结点两两hash构造Merkle树得到Merkle Root 最后与前区块hash,时间戳等信息一起再hash加密得到当前区块信息。

当数据被攻击篡改:

Merkle树构造算法:
设置Merkle树的基本架构,包括左右孩子,数据域,是否叶子几点和对应的信息主键

public class MerKleTreeNode 
    private MerKleTreeNode lChild;
    private MerKleTreeNode rChild;
    private String data;
    private Integer isLeaf;
    private Integer infoId;


先对叶子节点进行构造hash

for(Map<String,String> map : list)
            MerKleTreeNode node = new MerKleTreeNode(null,null,BlockHashAlgoUtils.encodeDataBySHA_256(map),1,-1);
            res.add(node);

由于Merkle二叉树的话可以利用他两两hash的缘故,构造一颗满二叉树,再利用满二叉树的特性(序号编排特点,树高公式,每层节点数公式)等等一切与满二叉树的特点,可以达到快速的存取MerkleTreeNode。既然要构造满二叉树,我们可以先利用叶子节点,通过求出一个数,设该数为 XX必须大于叶子节点数,且是2的幂次数(1,2,4,8…),且X不能太大,不然造成空间浪费,必须越接近叶子节点总数越好。

        if((size & (size -1))!=0)
            int n = size;
            n |= n >>> 1;
            n |= n >>> 2;
            n |= n >>> 4;
            n |= n >>> 8;
            n |= n >>> 16;
            n = n >= MAXIMUM ? MAXIMUM : n + 1;
            //计算差距
            int distance = n - size;
            for(int i =size-distance;i < size; i++)
                //复制对称数据,凑齐merkle树所有节点
                data.add(data.get(i));
            
            size = n;
        

上图例子如下:
init: 0001101011011001
>>>1: 0001101010000100 | 0000110101000010 = 000111111000110
````````(无符号右移类推)
====>>> 0001111111111111
res = 0001111111111111 + 1 = 0010000000000000 = 2^14

利用高位右移的特性,其实就是把传入进来的数通过无符号右移把他所有的位都变为1,最后在通过一次+1 ,既可以在原数的下一个最高位上得到一个1,且其余的位置因为2进制的缘故变为了0,这下就可以得到一个准确地 X

接着就是计算一些高度啊,序号,以及每层拥有的节点树
获取size的二进制形式,它的长度就是merkle树的高度,也就是该二进制的长度

  int height = Integer.toBinaryString(size).length();
	int idxStart = (total +1) >> 1;
	int total = (int)(Math.pow(2,height)-1);

最后构造Merkle树
idxStart作为计算每一层节点(从左数起)第一个叶子节点的序号(根节点从1开始编排)

while(height-1>0)
                /**
                 * 总节点数 2^n-1
                 * 每一层的最左节点序号
                 */
                total = (int)(Math.pow(2,height-1)-1);
                idxStart = (total +1) >> 1;
                /**
                 * 逐层构建,new temp(list) 作为新一层的数据赋值到data(list)上,循环
                 */
                ArrayList<MerKleTreeNode> temp = new ArrayList<>();
                for(int j = 0; j < size ; j+=2)
                    MerKleTreeNode lChild = data.get(j);
                    MerKleTreeNode rChild = data.get(j+1);
                    String hash = BlockHashAlgoUtils.encodeDataBySHA_256(lChild.getData() + rChild.getData());
                    MerKleTreeNode node = new MerKleTreeNode(lChild, rChild, hash,0,-1);
                    temp.add(node);
                    //输出非叶节点的序号
                    System.out.println(idxStart++);
                
            //temp 下个循环进行新建回收,赋值data
            data = temp;
            height--;
            //size长度每次减半
            size = size >> 1;
        

接下来是SPV验证,由于数组存储不方便展示,顾我把数据先行存放在数据库中,利用index字段存储数组的下标。
进行SPV查询,最重要的就是需要先获取SPV验证路径(即需要对两两hash的另一半作为固定的因素,如下图黑圈所示),还是根据满二叉树的序号特点,通过计算下标
如图所示,很明显,左节点都是偶数,右节点是奇数,根结点是1,可以利用这个特性,根据传入消息所在的index快速得到所有的spv验证路径节点的index

 while(idx>1)
                /**
                 * if 是偶数左节点,取他的右兄弟节点
                 * else 取左节点
                 * 完成后向父节点移动
                 */
                if((idx&1) == 0)
                    list.add(idx+1);
                else
                    list.add(idx-1);
                
                idx = idx >> 1;
            
            /**
             * 1也要加入进去
             */
            list.add(idx);
            //根据集合在list集合中的数据去数据库中查询所有的节点
         		........(查库操作)
         

接着就是再一次构造hash分支重新得到MerkleRoot了,与已经上传到区块链上的merkleROOT进行比对,既可以判断是否又被修改信息。

if(CollectionUtils.isEmpty(checkProofs))
            return res;
        
        /**
         * 从集合中取出一个,任意一个所属的区块和链都应相同
         */
        Integer blockIndex = checkProofs.get(0).getBlockIndex();
        MerkleNode merkleRoot = getMerkleRoot(blockIndex);
        if(Objects.isNull(merkleRoot))
            return res;
        
        /**
         * 与区块链上的block比对Merkle Root
         */
        Blockchain blockchain = blockchainMapper.selectByPrimaryKey(blockIndex);
        if(!blockchain.getBlockMerkle().equals(merkleRoot.getHash()))
            return res;
        
        /**
         *  与merkle树上的验证路径比对
         *  mapper降序处理
         *  通过奇偶判断左右孩子
         *  跑到最后一层即可,不用继续计算
         */
        Info info = infoMapper.selectByPrimaryKey(infoId);
        String hash = BlockHashAlgoUtils.encodeDataBySHA_256(info.toString());
        for(int i = checkProofs.size()-1;i>0;i--)
            MerkleNode node2 = checkProofs.get(i);
            if((node2.getMerkleNodeIndex()&1)==0)
                hash = BlockHashAlgoUtils.encodeDataBySHA_256(node2.getHash()+hash);
            else
                hash = BlockHashAlgoUtils.encodeDataBySHA_256(hash + node2.getHash());
            
        
        /**
         * 新生成的与merkle root再次比对
         */
        if(!blockchain.getBlockMerkle().equals(hash))
            return res;
        

总的来说,采用Merkle + SPV的好处就是当需要验证海量信息中某个信息是否遭到修改,可以无需得知所有节点的信息,只需要验证某个小分支的节点即可,所需要的节点只需要log(n+1)个,当数量级很大的时候,节省的空间是很可观的,用户也无需承担那么大的代价,他们只需要保留区块链的头部数据即可。
余下就是区块链的特点了,由于MerkleRoot保存在区块链中,所以对应的信息可基本可以保障他的可靠性(仍有可能发生hash碰撞的因素,看利用的hash算法)

以上是关于Merkle树的生成和SPV验证信息的策略的主要内容,如果未能解决你的问题,请参考以下文章

比特币区块结构Merkle树及简单支付验证分析

比特币区块结构Merkle树及简单支付验证分析

Merkle 树路径是如何生成的?

区块链 交易怎么验证是否被篡改 SPV验证

具有级别 db 日志合并树的 Merkle 树

比特币(BSV)知识库:支付- 简易支付验证(Simplified Payment Verification,SPV)