Skip to content

Commit d6aa26c

Browse files
committed
[ETCM-533] Proof of non-existence
1 parent f9a434d commit d6aa26c

File tree

3 files changed

+105
-18
lines changed

3 files changed

+105
-18
lines changed

src/main/scala/io/iohk/ethereum/mpt/MerklePatriciaTrie.scala

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ class MerklePatriciaTrie[K, V] private (private[mpt] val rootNode: Option[MptNod
103103
* @throws io.iohk.ethereum.mpt.MerklePatriciaTrie.MPTException if there is any inconsistency in how the trie is build.
104104
*/
105105
def getProof(key: K): Option[Vector[MptNode]] = {
106-
pathTraverse[Vector[MptNode]](Vector.empty, mkKeyNibbles(key)) { case (acc, node) =>
106+
pathTraverseWithProof[Vector[MptNode]](Vector.empty, mkKeyNibbles(key)) { case (acc, node) =>
107107
node match {
108108
case nextNodeOnExt @ (_: BranchNode | _: ExtensionNode | _: LeafNode) => acc :+ nextNodeOnExt
109109
case _ => acc
@@ -161,6 +161,46 @@ class MerklePatriciaTrie[K, V] private (private[mpt] val rootNode: Option[MptNod
161161
}
162162
}
163163

164+
private def pathTraverseWithProof[T](acc: T, searchKey: Array[Byte])(op: (T, MptNode) => T): Option[T] = {
165+
166+
@tailrec
167+
def pathTraverseWithProof(acc: T, node: MptNode, searchKey: Array[Byte], op: (T, MptNode) => T): Option[T] = {
168+
node match {
169+
case LeafNode(key, _, _, _, _) =>
170+
if (key.toArray[Byte] sameElements searchKey) Some(op(acc, node)) else Some(acc)
171+
172+
case extNode @ ExtensionNode(sharedKey, _, _, _, _) =>
173+
val (commonKey, remainingKey) = searchKey.splitAt(sharedKey.length)
174+
if (searchKey.length >= sharedKey.length && (sharedKey.toArray[Byte] sameElements commonKey)) {
175+
pathTraverseWithProof(op(acc, node), extNode.next, remainingKey, op)
176+
} else Some(acc)
177+
178+
case branch: BranchNode =>
179+
if (searchKey.isEmpty) Some(op(acc, node))
180+
else
181+
pathTraverseWithProof(
182+
op(acc, node),
183+
branch.children(searchKey(0)),
184+
searchKey.slice(1, searchKey.length),
185+
op
186+
)
187+
188+
case HashNode(bytes) =>
189+
pathTraverseWithProof(acc, getFromHash(bytes, nodeStorage), searchKey, op)
190+
191+
case NullNode =>
192+
Some(acc)
193+
}
194+
}
195+
196+
rootNode match {
197+
case Some(root) =>
198+
pathTraverseWithProof(acc, root, searchKey, op)
199+
case None =>
200+
None
201+
}
202+
}
203+
164204
private def getFromHash(nodeId: Array[Byte], source: MptStorage): MptNode = {
165205
val nodeEncoded = source.get(nodeId).encode
166206
MptTraversals

src/test/scala/io/iohk/ethereum/domain/BlockchainSpec.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,10 @@ class BlockchainSpec extends AnyFlatSpec with Matchers with ScalaCheckPropertyCh
152152
//unhappy path
153153
val wrongAddress = Address(666)
154154
val retrievedAccountProofWrong = blockchain.getAccountProof(wrongAddress, headerWithAcc.number)
155-
retrievedAccountProofWrong.isDefined shouldBe false
155+
//the account doesn't exist, so we can't retrieve it, but we do receive a proof of non-existence with a full path of nodes that we iterated
156+
retrievedAccountProofWrong.isDefined shouldBe true
157+
retrievedAccountProofWrong.size shouldBe 1
158+
mptWithAcc.get(wrongAddress) shouldBe None
156159

157160
//happy path
158161
val retrievedAccountProof = blockchain.getAccountProof(address, headerWithAcc.number)

src/test/scala/io/iohk/ethereum/mpt/MerklePatriciaTrieSuite.scala

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -554,22 +554,26 @@ class MerklePatriciaTrieSuite extends AnyFunSuite with ScalaCheckPropertyChecks
554554
assert(proof.isEmpty)
555555
}
556556

557-
test("getProof returns empty result for non-existing key") {
558-
forAll(keyValueListGen()) { keyValueList: Seq[(Int, Int)] =>
559-
val input: Seq[(Array[Byte], Array[Byte])] = keyValueList
560-
.map { case (k, v) => k.toString.getBytes() -> v.toString.getBytes() }
561-
562-
val trie = input
563-
.foldLeft(emptyMpt) { case (recTrie, (key, value)) =>
564-
recTrie.put(key, value)
565-
}
566-
567-
input.toList.foreach(x => {
568-
val keyToFind = x._1 ++ Array(Byte.MaxValue)
569-
val proof = trie.getProof(keyToFind)
570-
assert(proof.isEmpty)
571-
})
572-
}
557+
test("getProof returns proof result for non-existing address") {
558+
// given
559+
val EmptyTrie = MerklePatriciaTrie[Array[Byte], Array[Byte]](emptyEphemNodeStorage)
560+
val key1: Array[Byte] = Hex.decode("10000001")
561+
val key2: Array[Byte] = Hex.decode("10000002")
562+
val key3: Array[Byte] = Hex.decode("30000003")
563+
val key4: Array[Byte] = Hex.decode("10000004") //a key that doesn't have a corresponding value in the trie
564+
565+
val val1: Array[Byte] = Hex.decode("0101")
566+
val val2: Array[Byte] = Hex.decode("0102")
567+
val val3: Array[Byte] = Hex.decode("0103")
568+
val trie = EmptyTrie
569+
.put(key1, val1)
570+
.put(key2, val2)
571+
.put(key3, val3)
572+
// when
573+
val proof: Option[Vector[MptNode]] = trie.getProof(key4)
574+
// then
575+
assert(proof.isDefined)
576+
proof.foreach(_.foreach(MptUtils.showMptNode(_, true, " ")))
573577
}
574578

575579
test("getProof returns valid proof for existing key") {
@@ -620,3 +624,43 @@ class MerklePatriciaTrieSuite extends AnyFunSuite with ScalaCheckPropertyChecks
620624
assert(obtained.isEmpty)
621625
}
622626
}
627+
628+
object MptUtils {
629+
import io.iohk.ethereum.mpt._
630+
import org.bouncycastle.util.encoders.Hex
631+
def show(val1: Array[Byte]): String =
632+
Hex.encode(val1).map(_.toChar).mkString
633+
def showKV(key1: Array[Byte], trie: MerklePatriciaTrie[Array[Byte], Array[Byte]]): String =
634+
show(key1) + " -> " + trie.get(key1).map(show)
635+
def showMptNode(n: MptNode, showNull: Boolean = false, indent: String): Unit = {
636+
n match {
637+
case LeafNode(key, value, cachedHash, cachedRlpEncoded, parsedRlp) =>
638+
if (showNull) println("\n")
639+
println(s"$indent LeafNode(${show(key.toArray)} -> ${show(value.toArray)})")
640+
case ExtensionNode(sharedKey, next, cachedHash, cachedRlpEncoded, parsedRlp) =>
641+
if (showNull) println("\n")
642+
println(s"$indent ExtensionNode(sharedKey = ${show(sharedKey.toArray)}")
643+
println(s"$indent next = ")
644+
showMptNode(next, false, indent + " ")
645+
println(s"$indent )")
646+
case BranchNode(children, terminator, cachedHash, cachedRlpEncoded, parsedRlp) =>
647+
if (showNull) println("\n")
648+
println(
649+
s"$indent BranchNode(children = ${children.filterNot(n => n.isInstanceOf[NullNode.type]).size}, terminator = ${terminator
650+
.map(e => show(e.toArray))}"
651+
)
652+
println(s"$indent children: ")
653+
children.map(showMptNode(_, false, indent + " "))
654+
println(s"$indent )")
655+
case NullNode =>
656+
if (showNull) println(s"$indent NullNode")
657+
case other =>
658+
if (showNull) println("\n")
659+
println(s"$indent $other")
660+
}
661+
}
662+
def showMptTrie[K, V](mpt: MerklePatriciaTrie[K, V]): Unit = {
663+
println(s"MPT ROOT HASH ${mpt.getRootHash} rootNode: ")
664+
mpt.rootNode.map(n => showMptNode(n, false, " "))
665+
}
666+
}

0 commit comments

Comments
 (0)