认识MMR(Merkle Mountain Range)

MMR是什么

merkle tree一种二叉树也是区块链中一种常见的数据结构,其特性就是树的根及中间节点主要是由其左右子树的Hash构成。Parent = H(0,1),其以密码学保证其安全性,以相同顺序插入才能计算出最终一致的树根。


Merkle tree

而mmr(Merkle Mountain Range)是Peter Todd提出的一种Merkle tree,长相类似一组连续的山峰组成,其被设计为节点插入后就不能被修改,支持动态插入。


MMR(Merkle Mountain Range)

对于普通Merkle树对于每个新加入节点都需要重新计算merkle root,如果节点数量很大的话这个计算量会非常巨大,而mmr支持动态加入新节点并计算root。

MMR的组成

mmr构造

上图mmr包含了3个叶子节点,叶子节点的插入顺序就是叶子节点的标识(节点的存储位置),插入节点1后由于出现相同高度的树,则将节点0,1合并(2 = merge(0,1))生成父节点2,然后再插入节点3由于高度不一一致,无需树的合并,最后生成如图所示的MMR。

由上图可以发现,以存储索引位置作为其坐标的二叉树,都有左子树与父节点的距离(offset)为offset=2^Height,兄弟节点之间的距离为offset=2^Height - 1,这样就可以计算出任意节点的兄弟节点与父节点的坐标。

另外如果我们能够计算出任意节点的高度,我们就能计算出任意节点的父节点及兄弟节点的坐标了,将节点坐标从1开始并以二进制来表示。如图:

坐标为二进制表示的mmr

任意高度的最左侧节点1的数量就是其高度,由于最左侧子树每次合并右子树生成父节点增加高度,实际上是左节点坐标n计算出父节点坐标n<<1 + 1,这样计算任意节点的高度就可以表示为将节点向最左侧移动,即当其坐标全为1时,其数量就时其高度。如下代码获取任意坐标的所在高度:

func countZore(num uint64) int {
    return bits.UintSize - bits.OnesCount64(num)
}
func leadingZeros(num uint64) int {
    return bits.LeadingZeros64(num)
}
func allOnes(num uint64) bool {
    return num != 0 && countZore(num) == leadingZeros(num)
}
func jumpLeft(pos uint64) uint64 {
    bitLength := 64 - leadingZeros(pos)
    mostSignificantBits := uint64(1) << uint64(bitLength-1)
    return pos - (mostSignificantBits - 1)
}
func pos_height_in_tree(pos uint64) int {
    pos += 1
    for !allOnes(pos) {
        pos = jumpLeft(pos)
    }
    return 64 - leadingZeros(pos) - 1
}

现在我们可以顺序追加节点了,我们只需要判断下一个节点的高度,如果大于当前高度则需要合并左右子树,方法如下:

func (m *mmr) append(n *Node) *Node {
    height, pos := 0, m.cur_size
    n.index = pos
    m.values = append(m.values, n)
    for pos_height_in_tree(pos+1) > height {
        pos++
        // calculate pos of left child and right child
        left_pos := pos - parent_offset(height)
        right_pos := left_pos + sibling_offset(height)
        left, right := m.values[left_pos], m.values[right_pos]
        parent := &Node{index: pos}
        merge(parent, left, right)
        m.values = append(m.values, parent)
        height++
    }
    m.cur_size = pos + 1
    return n
}

MMR的证明

由图2可以知道,MMR可能会有多个山峰,而MMR的root是由最右侧的山峰依次向左合并,直到最后形成root,这个操作也被称为山峰的拱起操作。图2中的root=Hash(Hash(18,17),14)

MMR的root是由山峰的拱起得到,那么最左侧的山峰一定一个完全的二叉树,节点数量为2^Height - 1,由此我们可以在固定节点数量下(Count)不断尝试左侧山峰的高度,找到2^Height - 1 < Count的最大的树,如下:

func left_peak_pos_by_height(height int) uint64 {
        // -2 表示获取从0开始的山峰的坐标
    return (uint64(1) << uint64(height+1)) - 2
}
func left_peak_height_pos(mmrSize uint64) (int, uint64) {
    height := 0
    prev_pos := uint64(0)
    pos := left_peak_pos_by_height(height)
    //不断增加山峰的高度,直到最后一次山峰的坐标超过节点总数
    for pos < mmrSize {
        height += 1
        prev_pos = pos
        pos = left_peak_pos_by_height(height)
    }
    return height - 1, prev_pos
}

在计算出左侧山峰后,可以以此为坐标,依次计算出右侧的所有山峰,如下:

func get_right_peak(height int, peakPos, mmrSize uint64) (int, uint64) {
    //jump to right sibling
    peakPos += sibling_offset(height)
    //jump to left child
    for {
        if peakPos <= mmrSize-1 {
            break
        }
        if height == 0 {
            //no right peak exists
            return height, 0
        }
        height -= 1
        peakPos -= parent_offset(height)
    }
    return height, peakPos
}
func get_peaks(mmrSize uint64) []uint64 {
    res := make([]uint64, 0, 0)
    height, pos := left_peak_height_pos(mmrSize)
    res = append(res, pos)
    for height > 0 {
        height, pos = get_right_peak(height, pos, mmrSize)
        if height == 0 && pos == 0 {
            break
        }
        res = append(res, pos)
    }
    return res
}

获取到所有山峰后,就可以对所有山峰,由左到右依次拱起,最后得到MMR的root。如下:

func (m *mmr) getRoot() Hash {
    if m.cur_size == 0 {
        return Hash{0}
    }
    if m.cur_size == 1 {
        return m.values[0].getHash()
    }
    return m.bag_rhs_peaks(0, get_peaks(m.cur_size))
}
func (m *mmr) bag_rhs_peaks(pos uint64, peaks []uint64) Hash {
    rhs_peak_hashes := make([]Hash, 0, 0)
    for _, v := range peaks {
        if v > pos {
            rhs_peak_hashes = append(rhs_peak_hashes, m.values[v].getHash())
        }
    }
    for len(rhs_peak_hashes) > 1 {
        last := len(rhs_peak_hashes) - 1
        right := rhs_peak_hashes[last]
        rhs_peak_hashes = rhs_peak_hashes[:last]
        last = len(rhs_peak_hashes) - 1
        left := rhs_peak_hashes[last]
        rhs_peak_hashes = rhs_peak_hashes[:last]
        rhs_peak_hashes = append(rhs_peak_hashes, merge2(right, left))
    }
    if len(rhs_peak_hashes) == 1 {
        return rhs_peak_hashes[0]
    } else {
        return Hash{0}
    }
}

merkle proof

构造叶子节点的 merkle proof,分三个步骤:

  • 构造该叶子到山峰的proof

  • 将右侧的山峰加入proof

  • 将左侧的山峰从右到左加入proof.

如下:

func (m *mmr) gen_proof(pos uint64) *MerkleProof {
    proofs := make([]Hash, 0, 0)
    height := 0
    for pos < m.cur_size {
        pos_height, next_height := pos_height_in_tree(pos), pos_height_in_tree(pos+1)
        if next_height > pos_height {
            // get left child sib
            sib_pos := pos - sibling_offset(height)
            // break if sib is out of mmr
            if sib_pos >= m.cur_size {
                break
            }
            proofs = append(proofs, m.values[sib_pos].getHash())
            // goto parent node
            pos = pos + 1
        } else {
            // get right child
            sib_pos := pos + sibling_offset(height)
            // break if sib is out of mmr
            if sib_pos >= m.cur_size {
                break
            }
            proofs = append(proofs, m.values[sib_pos].getHash())
            // goto parent node
            next_pos := pos + parent_offset(height)
            pos = next_pos
        }
        height += 1
    }
    // now pos is peak of the mountain(because pos can't find a sibling)
    peak_pos := pos
    peaks := get_peaks(m.cur_size)
    // bagging rhs peaks into one hash
    rhs_peak_hash := m.bag_rhs_peaks(peak_pos, peaks)
    if !equal_hash(rhs_peak_hash, Hash{0}) {
        proofs = append(proofs, rhs_peak_hash)
    }
    // put left peaks to proof
    for i := len(peaks) - 1; i >= 0; i-- {
        p := peaks[i]
        if p < pos {
            proofs = append(proofs, m.values[p].getHash())
        }
    }
    return newMerkleProof(m.cur_size, proofs)
}

merkle verify

proof的验证,以相同的顺序重新计算Merkle Root就可以,如下:

func (m *MerkleProof) verify(root Hash, pos uint64, leaf_hash Hash) bool {
    peaks := get_peaks(m.mmrSize)
    height := 0
    for _, proof := range m.proofs {
        // verify bagging peaks
        if pos_in_peaks(pos, peaks) {
            if pos == peaks[len(peaks)-1] {
                leaf_hash = merge2(leaf_hash, proof)
            } else {
                leaf_hash = merge2(proof, leaf_hash)
                pos = peaks[len(peaks)-1]
            }
            continue
        }
        // verify merkle path
        pos_height, next_height := pos_height_in_tree(pos), pos_height_in_tree(pos+1)
        if next_height > pos_height {
            // we are in right child
            leaf_hash = merge2(proof, leaf_hash)
            pos += 1
        } else {
            leaf_hash = merge2(leaf_hash, proof)
            pos += parent_offset(height)
        }
        height += 1
    }
    return equal_hash(leaf_hash, root)
}

MMR的作用

MMR可以极大的减少merkle证明的数据量,可以大幅度的减轻存储和网络的负担,提升验证效率,目前Open timestamp 和 Grin 等项目及Fly client的论文中都使用了MMR的证明。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345