Skip to content

Commit 40ab5fd

Browse files
committed
Merge branch 'master' of github.com:input-output-hk/scrypto into slicedavltree-rework
2 parents 5e5f095 + f260a09 commit 40ab5fd

File tree

3 files changed

+202
-4
lines changed

3 files changed

+202
-4
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package scorex.crypto.authds.merkle
2+
3+
import scorex.crypto.authds.Side
4+
import scorex.crypto.authds.merkle.MerkleTree.InternalNodePrefix
5+
import scorex.crypto.hash.{CryptographicHash, Digest}
6+
import scorex.util.ScorexEncoding
7+
8+
import scala.language.postfixOps
9+
10+
/**
11+
* Implementation is based on Compact Merkle Multiproofs by Lum Ramabaja
12+
* Retrieved from https://deepai.org/publication/compact-merkle-multiproofs
13+
*
14+
* @param indices - leaf indices used to build the proof
15+
* @param proofs - hash and side of nodes in the proof
16+
*/
17+
case class BatchMerkleProof[D <: Digest](indices: Seq[(Int, Digest)], proofs: Seq[(Digest, Side)])
18+
(implicit val hf: CryptographicHash[D]) extends ScorexEncoding {
19+
20+
/**
21+
* Validate BatchMerkleProof against an expected root hash
22+
*
23+
* @param expectedRootHash - BatchMerkleProof should evaluate to this hash
24+
* @return true or false (Boolean)
25+
*/
26+
def valid(expectedRootHash: Digest): Boolean = {
27+
28+
/**
29+
* Recursive function to validate the multiproof
30+
*
31+
* @param a - leaf indices
32+
* @param e - sorted pairs of (index, hash) of the leaves
33+
* @param m - hashes of the multiproof
34+
* @return true or false (Boolean)
35+
*/
36+
def loop(a: Seq[Int], e: Seq[(Int, Digest)], m: Seq[(Digest, Side)]): Seq[Digest] = {
37+
38+
// For each of the indices in A, take the index of its immediate neighbor
39+
// Store the given element index and the neighboring index as a pair of indices
40+
val b = a
41+
.map(i => {
42+
if (i % 2 == 0) {
43+
(i, i + 1)
44+
} else {
45+
(i - 1, i)
46+
}
47+
})
48+
49+
// B will always have the same size as E
50+
assert(e.size == b.size)
51+
52+
var a_new: Seq[Int] = Seq.empty
53+
var e_new: Seq[Digest] = Seq.empty
54+
var m_new = m
55+
56+
var i = 0
57+
58+
// assign generated hashes to a new E that will be used for the next iteration
59+
while (i < b.size) {
60+
61+
// check for duplicate index pairs inside b
62+
if (b.size > 1 && b.lift(i) == b.lift(i + 1)) {
63+
64+
// hash the corresponding values inside E with one another
65+
e_new = e_new :+ hf.prefixedHash(InternalNodePrefix, e.apply(i)._2 ++ e.apply(i + 1)._2)
66+
i += 2
67+
} else {
68+
69+
// hash the corresponding value inside E with the first hash inside M, taking note of the side
70+
if (m_new.head._2 == MerkleProof.LeftSide) {
71+
e_new = e_new :+ hf.prefixedHash(MerkleTree.InternalNodePrefix, m_new.head._1 ++ e.apply(i)._2)
72+
} else {
73+
e_new = e_new :+ hf.prefixedHash(MerkleTree.InternalNodePrefix, e.apply(i)._2 ++ m_new.head._1)
74+
}
75+
76+
// remove the used value from m
77+
m_new = m_new.drop(1)
78+
i += 1
79+
}
80+
}
81+
82+
// Take all the even numbers from B_pruned, and divide them by two
83+
a_new = b.distinct.map(_._1 / 2)
84+
85+
// Repeat until the root of the tree is reached (M has no more elements)
86+
if (m_new.nonEmpty || e_new.size > 1) {
87+
e_new = loop(a_new, a_new zip e_new, m_new)
88+
}
89+
e_new
90+
}
91+
92+
val e = indices sortBy(_._1)
93+
loop(indices.map(_._1), e, proofs).head.sameElements(expectedRootHash)
94+
}
95+
}

src/main/scala/scorex/crypto/authds/merkle/MerkleTree.scala

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package scorex.crypto.authds.merkle
22

3+
import scorex.crypto.authds.merkle.MerkleTree.InternalNodePrefix
34
import scorex.crypto.authds.{LeafData, Side}
4-
import scorex.crypto.hash._
5+
import scorex.crypto.hash.{Digest, _}
56

67
import scala.annotation.tailrec
78
import scala.collection.mutable
@@ -38,6 +39,72 @@ case class MerkleTree[D <: Digest](topNode: Node[D],
3839
None
3940
}
4041

42+
/**
43+
* Build compact batch Merkle proof by indices
44+
*
45+
* @param indices - leaf indices
46+
* @return Optional BatchMerkleProof
47+
*/
48+
def proofByIndices(indices: Seq[Int])(implicit hf: CryptographicHash[D]): Option[BatchMerkleProof[D]] = {
49+
50+
/**
51+
* Recursive function to build the multiproof
52+
*
53+
* @param a - leaf indices
54+
* @param l - hashes of the current layer of the Merkle tree
55+
* @return multiproof as sequence of pairs (hash, side)
56+
*/
57+
58+
def loop(a: Seq[Int], l: Seq[Digest]): Seq[(Digest, Side)] = {
59+
60+
// For each of the indices in A, take the index of its immediate neighbor in layer L
61+
// Store the given element index and the neighboring index as a pair of indices
62+
// Remove any duplicates
63+
val b_pruned = a
64+
.map(i => {
65+
if (i % 2 == 0) {
66+
(i, i + 1)
67+
} else {
68+
(i - 1, i)
69+
}
70+
})
71+
.distinct
72+
73+
// Take the difference between the set of indices in B_pruned and A
74+
// Append the hash values (and side) for the given indices in the current Merkle layer to the multiproof M
75+
val b_flat = b_pruned.flatten{case (a,b) => Seq(a,b)}
76+
val dif = b_flat diff a
77+
var m = dif.map(i => {
78+
val side = if (i % 2 == 0) MerkleProof.LeftSide else MerkleProof.RightSide
79+
(l.lift(i).getOrElse(EmptyNode[D].hash), side)
80+
})
81+
82+
// Take all the even numbers from B_pruned, and divide them by two
83+
val a_new = b_pruned.map(_._1 / 2)
84+
85+
// Go up one layer in the tree
86+
val l_new = l.grouped(2).map(lr => {
87+
hf.prefixedHash(InternalNodePrefix,
88+
lr.head, if (lr.lengthCompare(2) == 0) lr.last else EmptyNode[D].hash)
89+
}).toSeq
90+
91+
// Repeat until the root of the tree is reached
92+
if (l_new.size > 1) {
93+
m = m ++ loop(a_new, l_new)
94+
}
95+
m
96+
}
97+
98+
if (indices.forall(index => index >= 0 && index < length)) {
99+
val hashes: Seq[Digest] = elementsHashIndex.toSeq.sortBy(_._2).map(_._1.toArray.asInstanceOf[Digest])
100+
val normalized_indices = indices.distinct.sorted
101+
val multiproof = loop(normalized_indices, hashes)
102+
Some(BatchMerkleProof(normalized_indices zip (normalized_indices map hashes.apply), multiproof))
103+
} else {
104+
None
105+
}
106+
}
107+
41108
lazy val lengthWithEmptyLeafs: Int = {
42109
def log2(x: Double): Double = math.log(x) / math.log(2)
43110

@@ -48,7 +115,7 @@ case class MerkleTree[D <: Digest](topNode: Node[D],
48115
override lazy val toString: String = {
49116
def loop(nodes: Seq[Node[D]], level: Int, acc: String): String = {
50117
if (nodes.nonEmpty) {
51-
val thisLevStr = s"Level $level: " + nodes.map(_.toString).mkString(",") + "\n"
118+
val thisLevStr = s"Level $level: " + nodes.map(_.hash.toList).map(_.toString).mkString(",") + "\n"
52119
val nextLevNodes = nodes.flatMap {
53120
case i: InternalNode[D] => Seq(i.left, i.right)
54121
case _ => Seq()

src/test/scala/scorex/crypto/authds/merkle/MerkleTreeSpecification.scala

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import org.scalatest.propspec.AnyPropSpec
44
import org.scalatest.matchers.should.Matchers
55
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks
66
import scorex.crypto.TestingCommons
7-
import scorex.crypto.authds.LeafData
8-
import scorex.crypto.hash.Keccak256
7+
import scorex.crypto.authds.{EmptyByteArray, LeafData}
8+
import scorex.crypto.authds.merkle.MerkleTree.InternalNodePrefix
9+
import scorex.crypto.hash.{Digest, Keccak256}
10+
11+
import scala.util.Random
912

1013
class MerkleTreeSpecification extends AnyPropSpec with ScalaCheckDrivenPropertyChecks with Matchers with TestingCommons {
1114
implicit val hf = Keccak256
@@ -46,6 +49,39 @@ class MerkleTreeSpecification extends AnyPropSpec with ScalaCheckDrivenPropertyC
4649
}
4750
}
4851

52+
property("Batch proof generation by indices") {
53+
val r = new Random()
54+
forAll(smallInt) { N: Int =>
55+
whenever(N > 0) {
56+
val d = (0 until N).map(_ => LeafData @@ scorex.utils.Random.randomBytes(LeafSize))
57+
val tree = MerkleTree(d)
58+
val randIndices = (0 until r.nextInt(N + 1) + 1)
59+
.map(_ => r.nextInt(N))
60+
.distinct
61+
.sorted
62+
tree.proofByIndices(randIndices).get.valid(tree.rootHash) shouldBe true
63+
}
64+
}
65+
}
66+
67+
property("Batch proof generation by duplicated indices") {
68+
val d = (0 until 10).map(_ => LeafData @@ scorex.utils.Random.randomBytes(LeafSize))
69+
val tree = MerkleTree(d)
70+
tree.proofByIndices(Seq(2,2,2,3,6,6,8,9,9)).get.valid(tree.rootHash) shouldBe true
71+
}
72+
73+
property("Batch proof generation by negative indices") {
74+
val d = (0 until 5).map(_ => LeafData @@ scorex.utils.Random.randomBytes(LeafSize))
75+
val tree = MerkleTree(d)
76+
tree.proofByIndices(Seq(-1,2)) shouldBe None
77+
}
78+
79+
property("Batch proof generation by oob indices") {
80+
val d = (0 until 5).map(_ => LeafData @@ scorex.utils.Random.randomBytes(LeafSize))
81+
val tree = MerkleTree(d)
82+
tree.proofByIndices(Seq(2,10)) shouldBe None
83+
}
84+
4985
property("Tree creation from 0 elements") {
5086
val tree = MerkleTree(Seq.empty)(hf)
5187
tree.rootHash shouldEqual Array.fill(hf.DigestSize)(0: Byte)

0 commit comments

Comments
 (0)