nimbus-eth2/beacon_chain/merkle_minimal.nim

204 lines
7.3 KiB
Nim

# beacon_chain
# Copyright (c) 2018-2020 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.3/tests/core/pyspec/eth2spec/utils/merkle_minimal.py
# Merkle tree helpers
# ---------------------------------------------------------------
{.push raises: [Defect].}
import
sequtils, strutils, macros, bitops,
# Specs
../../beacon_chain/spec/[beaconstate, datatypes, digest, helpers],
../../beacon_chain/ssz/merkleization
# TODO All tests need to be moved to the test suite.
func round_step_down(x: Natural, step: static Natural): int {.inline.} =
## Round the input to the previous multiple of "step"
when (step and (step - 1)) == 0:
# Step is a power of 2. (If compiler cannot prove that x>0 it does not make the optim)
x and not(step - 1)
else:
x - x mod step
type SparseMerkleTree*[Depth: static int] = object
## Sparse Merkle tree
# There is an extra "depth" layer to store leaf nodes
# This stores leaves at depth = 0
# and the root hash at the last depth
nnznodes: array[Depth+1, seq[Eth2Digest]] # nodes that leads to non-zero leaves
func merkleTreeFromLeaves(
values: openarray[Eth2Digest],
Depth: static[int] = DEPOSIT_CONTRACT_TREE_DEPTH
): SparseMerkleTree[Depth] =
## Depth should be the same as is_valid_merkle_branch
result.nnznodes[0] = @values
for depth in 1 .. Depth: # Inclusive range
let prev_depth_len = result.nnznodes[depth-1].len
let stop = round_step_down(prev_depth_len, 2)
for i in countup(0, stop-1, 2):
# hash by pair of previous nodes
let nodeHash = withEth2Hash:
h.update result.nnznodes[depth-1][i]
h.update result.nnznodes[depth-1][i+1]
result.nnznodes[depth].add nodeHash
if prev_depth_len != stop:
# If length is odd, the last one was skipped,
# we need to combine it
# with the zeroHash corresponding to the current depth
let nodeHash = withEth2Hash:
h.update result.nnznodes[depth-1][^1]
h.update zeroHashes[depth-1]
result.nnznodes[depth].add nodeHash
func getMerkleProof[Depth: static int](tree: SparseMerkleTree[Depth],
index: int,
depositMode = false): array[Depth, Eth2Digest] =
# Descend down the tree according to the bit representation
# of the index:
# - 0 --> go left
# - 1 --> go right
let path = uint32(index)
# This is what the nnznodes[depth].len would be if `index` had been the last
# deposit on the Merkle tree
var depthLen = index + 1
for depth in 0 ..< Depth:
let nodeIdx = int((path shr depth) xor 1)
# depositMode simulates only having constructed SparseMerkleTree[Depth]
# through exactly deposit specified.
if nodeIdx < tree.nnznodes[depth].len and
(nodeIdx < depthLen or not depositMode):
result[depth] = tree.nnznodes[depth][nodeIdx]
else:
result[depth] = zeroHashes[depth]
# Round up, i.e. a half-pair of Merkle nodes/leaves still requires a node
# in the next Merkle tree layer calculated
depthLen = (depthLen + 1) div 2
func attachMerkleProofs*(deposits: var openarray[Deposit]) =
let
deposit_data_roots = mapIt(deposits, it.data.hash_tree_root)
merkle_tree = merkleTreeFromLeaves(deposit_data_roots)
var
deposit_data_sums: seq[Eth2Digest]
for prefix_root in hash_tree_roots_prefix(
deposit_data_roots, 1'i64 shl DEPOSIT_CONTRACT_TREE_DEPTH):
deposit_data_sums.add prefix_root
for val_idx in 0 ..< deposits.len:
deposits[val_idx].proof[0..31] = merkle_tree.getMerkleProof(val_idx, true)
deposits[val_idx].proof[32].data[0..7] = uint_to_bytes8((val_idx + 1).uint64)
doAssert is_valid_merkle_branch(
deposit_data_roots[val_idx], deposits[val_idx].proof,
DEPOSIT_CONTRACT_TREE_DEPTH + 1, val_idx.uint64,
deposit_data_sums[val_idx])
proc testMerkleMinimal*(): bool =
proc toDigest[N: static int](x: array[N, byte]): Eth2Digest =
result.data[0 .. N-1] = x
let a = [byte 0x01, 0x02, 0x03].toDigest
let b = [byte 0x04, 0x05, 0x06].toDigest
let c = [byte 0x07, 0x08, 0x09].toDigest
block: # SSZ Sanity checks vs Python impl
block: # 3 leaves
let leaves = List[Eth2Digest, 3](@[a, b, c])
let root = hash_tree_root(leaves)
doAssert $root == "9ff412e827b7c9d40fc7df2725021fd579ab762581d1ff5c270316682868456e".toUpperAscii
block: # 2^3 leaves
let leaves = List[Eth2Digest, int64(1 shl 3)](@[a, b, c])
let root = hash_tree_root(leaves)
doAssert $root == "5248085b588fab1dd1e03f3cd62201602b12e6560665935964f46e805977e8c5".toUpperAscii
block: # 2^10 leaves
let leaves = List[Eth2Digest, int64(1 shl 10)](@[a, b, c])
let root = hash_tree_root(leaves)
doAssert $root == "9fb7d518368dc14e8cc588fb3fd2749beef9f493fef70ae34af5721543c67173".toUpperAscii
block: # Round-trips
# TODO: there is an issue (also in EF specs?)
# using hash_tree_root([a, b, c])
# doesn't give the same hash as
# - hash_tree_root(@[a, b, c])
# - sszList(@[a, b, c], int64(nleaves))
# which both have the same hash.
#
# hash_tree_root([a, b, c]) gives the same hash as
# the last hash of merkleTreeFromLeaves
#
# Running tests with hash_tree_root([a, b, c])
# works for depth 2 (3 or 4 leaves)
macro roundTrips(): untyped =
result = newStmtList()
# compile-time unrolled test
for nleaves in [3, 4, 5, 7, 8, 1 shl 10, 1 shl 32]:
let depth = fastLog2(nleaves-1) + 1
result.add quote do:
block:
let tree = merkleTreeFromLeaves([a, b, c], Depth = `depth`)
#echo "Tree: ", tree
doAssert tree.nnznodes[`depth`].len == 1
let root = tree.nnznodes[`depth`][0]
#echo "Root: ", root
block: # proof for a
let index = 0
doAssert is_valid_merkle_branch(
a, get_merkle_proof(tree, index = index),
depth = `depth`,
index = index.uint64,
root = root
), "Failed (depth: " & $`depth` &
", nleaves: " & $`nleaves` & ')'
block: # proof for b
let index = 1
doAssert is_valid_merkle_branch(
b, get_merkle_proof(tree, index = index),
depth = `depth`,
index = index.uint64,
root = root
), "Failed (depth: " & $`depth` &
", nleaves: " & $`nleaves` & ')'
block: # proof for c
let index = 2
doAssert is_valid_merkle_branch(
c, get_merkle_proof(tree, index = index),
depth = `depth`,
index = index.uint64,
root = root
), "Failed (depth: " & $`depth` &
", nleaves: " & $`nleaves` & ')'
roundTrips()
true
when isMainModule:
discard testMerkleMinimal()