Skip to content

Commit 10f1612

Browse files
authored
Merge pull request #899 from input-output-hk/feature/ETCM-533-non-membership-proof
Feature/etcm 533 non membership proof
2 parents 5ea1b6f + 72b6a87 commit 10f1612

File tree

10 files changed

+266
-132
lines changed

10 files changed

+266
-132
lines changed

src/it/scala/io/iohk/ethereum/txExecTest/util/DumpChainApp.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package io.iohk.ethereum.txExecTest.util
22

33
import java.time.Clock
44
import java.util.concurrent.atomic.AtomicReference
5-
65
import akka.actor.ActorSystem
76
import akka.util.ByteString
87
import com.typesafe.config.ConfigFactory
@@ -15,6 +14,7 @@ import io.iohk.ethereum.db.storage.pruning.{ArchivePruning, PruningMode}
1514
import io.iohk.ethereum.db.storage.{AppStateStorage, StateStorage}
1615
import io.iohk.ethereum.domain.BlockHeader.HeaderExtraFields.HefEmpty
1716
import io.iohk.ethereum.domain.{Blockchain, UInt256, _}
17+
import io.iohk.ethereum.jsonrpc.ProofService.{EmptyStorageValueProof, StorageProof, StorageProofKey, StorageValueProof}
1818
import io.iohk.ethereum.ledger.{InMemoryWorldStateProxy, InMemoryWorldStateProxyStorage}
1919
import io.iohk.ethereum.mpt.MptNode
2020
import io.iohk.ethereum.network.EtcPeerManagerActor.PeerInfo
@@ -150,7 +150,7 @@ class BlockchainMock(genesisHash: ByteString) extends Blockchain {
150150
rootHash: NodeHash,
151151
position: BigInt,
152152
ethCompatibleStorage: Boolean
153-
): Option[(BigInt, Seq[MptNode])] = None
153+
): StorageProof = EmptyStorageValueProof(StorageProofKey(position))
154154

155155
override protected def getHashByBlockNumber(number: BigInt): Option[ByteString] = Some(genesisHash)
156156

src/main/scala/io/iohk/ethereum/domain/Blockchain.scala

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package io.iohk.ethereum.domain
22

33
import java.util.concurrent.atomic.AtomicReference
4-
54
import akka.util.ByteString
65
import cats.syntax.flatMap._
76
import cats.instances.option._
@@ -13,6 +12,7 @@ import io.iohk.ethereum.db.storage._
1312
import io.iohk.ethereum.db.storage.pruning.PruningMode
1413
import io.iohk.ethereum.domain
1514
import io.iohk.ethereum.domain.BlockchainImpl.BestBlockLatestCheckpointNumbers
15+
import io.iohk.ethereum.jsonrpc.ProofService.StorageProof
1616
import io.iohk.ethereum.ledger.{InMemoryWorldStateProxy, InMemoryWorldStateProxyStorage}
1717
import io.iohk.ethereum.mpt.{MerklePatriciaTrie, MptNode}
1818
import io.iohk.ethereum.utils.{ByteStringUtils, Logger}
@@ -95,11 +95,17 @@ trait Blockchain {
9595
*/
9696
def getAccountStorageAt(rootHash: ByteString, position: BigInt, ethCompatibleStorage: Boolean): ByteString
9797

98+
/**
99+
* Get a storage-value and its proof being the path from the root node until the last matching node.
100+
*
101+
* @param rootHash storage root hash
102+
* @param position storage position
103+
*/
98104
def getStorageProofAt(
99105
rootHash: ByteString,
100106
position: BigInt,
101107
ethCompatibleStorage: Boolean
102-
): Option[(BigInt, Seq[MptNode])]
108+
): StorageProof
103109

104110
/**
105111
* Returns the receipts based on a block hash
@@ -307,16 +313,15 @@ class BlockchainImpl(
307313
rootHash: ByteString,
308314
position: BigInt,
309315
ethCompatibleStorage: Boolean
310-
): Option[(BigInt, Seq[MptNode])] = {
316+
): StorageProof = {
311317
val storage: MptStorage = stateStorage.getBackingStorage(0)
312318
val mpt: MerklePatriciaTrie[BigInt, BigInt] = {
313319
if (ethCompatibleStorage) domain.EthereumUInt256Mpt.storageMpt(rootHash, storage)
314320
else domain.ArbitraryIntegerMpt.storageMpt(rootHash, storage)
315321
}
316-
for {
317-
value <- mpt.get(position)
318-
proof <- mpt.getProof(position)
319-
} yield (value, proof)
322+
val value: Option[BigInt] = mpt.get(position)
323+
val proof: Option[Vector[MptNode]] = mpt.getProof(position)
324+
StorageProof(position, value, proof)
320325
}
321326

322327
private def persistBestBlocksData(): Unit = {

src/main/scala/io/iohk/ethereum/jsonrpc/EthProofJsonMethodsImplicits.scala

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ import io.iohk.ethereum.jsonrpc.JsonRpcError.InvalidParams
44
import io.iohk.ethereum.jsonrpc.ProofService.{GetProofRequest, GetProofResponse, StorageProofKey}
55
import io.iohk.ethereum.jsonrpc.serialization.JsonEncoder
66
import io.iohk.ethereum.jsonrpc.serialization.JsonMethodDecoder
7-
import org.json4s.Extraction
87
import org.json4s.JsonAST.{JArray, JString, JValue, _}
9-
import org.json4s.JsonDSL._
108

119
object EthProofJsonMethodsImplicits extends JsonMethodsImplicits {
1210
def extractStorageKeys(input: JValue): Either[JsonRpcError, Seq[StorageProofKey]] = {

src/main/scala/io/iohk/ethereum/jsonrpc/EthProofService.scala

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import akka.util.ByteString
44
import cats.implicits._
55
import io.iohk.ethereum.consensus.blocks.BlockGenerator
66
import io.iohk.ethereum.domain.{Account, Address, Block, Blockchain, UInt256}
7+
import io.iohk.ethereum.jsonrpc.ProofService.StorageProof.asRlpSerializedNode
78
import io.iohk.ethereum.jsonrpc.ProofService.{
89
GetProofRequest,
910
GetProofResponse,
1011
ProofAccount,
1112
StorageProof,
12-
StorageProofKey
13+
StorageProofKey,
14+
StorageValueProof
1315
}
1416
import io.iohk.ethereum.mpt.{MptNode, MptTraversals}
1517
import monix.eval.Task
@@ -30,8 +32,26 @@ object ProofService {
3032

3133
case class GetProofResponse(proofAccount: ProofAccount)
3234

33-
/** The key used to get the storage slot in its account tree */
34-
case class StorageProofKey(v: BigInt) extends AnyVal
35+
sealed trait StorageProof {
36+
def key: StorageProofKey
37+
def value: BigInt
38+
def proof: Seq[ByteString]
39+
}
40+
41+
object StorageProof {
42+
def apply(position: BigInt, value: Option[BigInt], proof: Option[Vector[MptNode]]): StorageProof =
43+
(value, proof) match {
44+
case (Some(value), Some(proof)) =>
45+
StorageValueProof(StorageProofKey(position), value, proof.map(asRlpSerializedNode))
46+
case (None, Some(proof)) =>
47+
EmptyStorageValue(StorageProofKey(position), proof.map(asRlpSerializedNode))
48+
case (Some(value), None) => EmptyStorageProof(StorageProofKey(position), value)
49+
case (None, None) => EmptyStorageValueProof(StorageProofKey(position))
50+
}
51+
52+
def asRlpSerializedNode(node: MptNode): ByteString =
53+
ByteString(MptTraversals.encodeNode(node))
54+
}
3555

3656
/**
3757
* Object proving a relationship of a storage value to an account's storageHash
@@ -40,11 +60,20 @@ object ProofService {
4060
* @param value the value of the storage slot in its account tree
4161
* @param proof the set of node values needed to traverse a patricia merkle tree (from root to leaf) to retrieve a value
4262
*/
43-
case class StorageProof(
44-
key: StorageProofKey,
45-
value: BigInt,
46-
proof: Seq[ByteString]
47-
)
63+
case class EmptyStorageValueProof(key: StorageProofKey) extends StorageProof {
64+
val value: BigInt = BigInt(0)
65+
val proof: Seq[ByteString] = Seq.empty[MptNode].map(asRlpSerializedNode)
66+
}
67+
case class EmptyStorageValue(key: StorageProofKey, proof: Seq[ByteString]) extends StorageProof {
68+
val value: BigInt = BigInt(0)
69+
}
70+
case class EmptyStorageProof(key: StorageProofKey, value: BigInt) extends StorageProof {
71+
val proof: Seq[ByteString] = Seq.empty[MptNode].map(asRlpSerializedNode)
72+
}
73+
case class StorageValueProof(key: StorageProofKey, value: BigInt, proof: Seq[ByteString]) extends StorageProof
74+
75+
/** The key used to get the storage slot in its account tree */
76+
case class StorageProofKey(v: BigInt) extends AnyVal
4877

4978
/**
5079
* The merkle proofs of the specified account connecting them to the blockhash of the block specified.
@@ -143,14 +172,14 @@ class EthProofService(blockchain: Blockchain, blockGenerator: BlockGenerator, et
143172
blockchain.getAccountProof(address, blockNumber).map(_.map(asRlpSerializedNode)),
144173
noAccountProof(address, blockNumber)
145174
)
146-
storageProof <- getStorageProof(account, storageKeys)
175+
storageProof = getStorageProof(account, storageKeys)
147176
} yield ProofAccount(account, accountProof, storageProof, address)
148177
}
149178

150179
def getStorageProof(
151180
account: Account,
152181
storageKeys: Seq[StorageProofKey]
153-
): Either[JsonRpcError, Seq[StorageProof]] = {
182+
): Seq[StorageProof] = {
154183
storageKeys.toList
155184
.map { storageKey =>
156185
blockchain
@@ -159,21 +188,14 @@ class EthProofService(blockchain: Blockchain, blockGenerator: BlockGenerator, et
159188
position = storageKey.v,
160189
ethCompatibleStorage = ethCompatibleStorage
161190
)
162-
.map { case (value, proof) => StorageProof(storageKey, value, proof.map(asRlpSerializedNode)) }
163-
.toRight(noStorageProof(account, storageKey))
164191
}
165-
.sequence
166-
.map(_.toSeq)
167192
}
168193

169-
private def noStorageProof(account: Account, storagekey: StorageProofKey): JsonRpcError =
170-
JsonRpcError.LogicError(s"No storage proof for [${account.toString}] storage key [${storagekey.toString}]")
171-
172194
private def noAccount(address: Address, blockNumber: BigInt): JsonRpcError =
173-
JsonRpcError.LogicError(s"No storage proof for Address [${address.toString}] blockNumber [${blockNumber.toString}]")
195+
JsonRpcError.LogicError(s"No account found for Address [${address.toString}] blockNumber [${blockNumber.toString}]")
174196

175197
private def noAccountProof(address: Address, blockNumber: BigInt): JsonRpcError =
176-
JsonRpcError.LogicError(s"No storage proof for Address [${address.toString}] blockNumber [${blockNumber.toString}]")
198+
JsonRpcError.LogicError(s"No account proof for Address [${address.toString}] blockNumber [${blockNumber.toString}]")
177199

178200
private def asRlpSerializedNode(node: MptNode): ByteString =
179201
ByteString(MptTraversals.encodeNode(node))

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

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import io.iohk.ethereum.rlp.RLPImplicits._
99
import io.iohk.ethereum.rlp.{encode => encodeRLP}
1010
import org.bouncycastle.util.encoders.Hex
1111
import io.iohk.ethereum.utils.ByteUtils.matchingLength
12-
1312
import scala.annotation.tailrec
1413

1514
object MerklePatriciaTrie {
@@ -82,16 +81,14 @@ class MerklePatriciaTrie[K, V] private (private[mpt] val rootNode: Option[MptNod
8281
* @throws io.iohk.ethereum.mpt.MerklePatriciaTrie.MPTException if there is any inconsistency in how the trie is build.
8382
*/
8483
def get(key: K): Option[V] = {
85-
pathTraverse[Option[V]](None, mkKeyNibbles(key)) { case (_, node) =>
86-
node match {
87-
case LeafNode(_, value, _, _, _) =>
88-
Some(vSerializer.fromBytes(value.toArray[Byte]))
84+
pathTraverse[Option[V]](None, mkKeyNibbles(key)) {
85+
case (_, Some(LeafNode(_, value, _, _, _))) =>
86+
Some(vSerializer.fromBytes(value.toArray[Byte]))
8987

90-
case BranchNode(_, terminator, _, _, _) =>
91-
terminator.map(term => vSerializer.fromBytes(term.toArray[Byte]))
88+
case (_, Some(BranchNode(_, terminator, _, _, _))) =>
89+
terminator.map(term => vSerializer.fromBytes(term.toArray[Byte]))
9290

93-
case _ => None
94-
}
91+
case _ => None
9592
}.flatten
9693
}
9794

@@ -105,7 +102,8 @@ class MerklePatriciaTrie[K, V] private (private[mpt] val rootNode: Option[MptNod
105102
def getProof(key: K): Option[Vector[MptNode]] = {
106103
pathTraverse[Vector[MptNode]](Vector.empty, mkKeyNibbles(key)) { case (acc, node) =>
107104
node match {
108-
case nextNodeOnExt @ (_: BranchNode | _: ExtensionNode | _: LeafNode) => acc :+ nextNodeOnExt
105+
case Some(nextNodeOnExt @ (_: BranchNode | _: ExtensionNode | _: LeafNode | _: HashNode)) =>
106+
acc :+ nextNodeOnExt
109107
case _ => acc
110108
}
111109
}
@@ -121,25 +119,25 @@ class MerklePatriciaTrie[K, V] private (private[mpt] val rootNode: Option[MptNod
121119
* @tparam T accumulator type
122120
* @return accumulated data or None if key doesn't exist
123121
*/
124-
private def pathTraverse[T](acc: T, searchKey: Array[Byte])(op: (T, MptNode) => T): Option[T] = {
122+
private def pathTraverse[T](acc: T, searchKey: Array[Byte])(op: (T, Option[MptNode]) => T): Option[T] = {
125123

126124
@tailrec
127-
def pathTraverse(acc: T, node: MptNode, searchKey: Array[Byte], op: (T, MptNode) => T): Option[T] = {
125+
def pathTraverse(acc: T, node: MptNode, searchKey: Array[Byte], op: (T, Option[MptNode]) => T): Option[T] = {
128126
node match {
129127
case LeafNode(key, _, _, _, _) =>
130-
if (key.toArray[Byte] sameElements searchKey) Some(op(acc, node)) else None
128+
if (key.toArray[Byte] sameElements searchKey) Some(op(acc, Some(node))) else Some(op(acc, None))
131129

132130
case extNode @ ExtensionNode(sharedKey, _, _, _, _) =>
133131
val (commonKey, remainingKey) = searchKey.splitAt(sharedKey.length)
134132
if (searchKey.length >= sharedKey.length && (sharedKey.toArray[Byte] sameElements commonKey)) {
135-
pathTraverse(op(acc, node), extNode.next, remainingKey, op)
136-
} else None
133+
pathTraverse(op(acc, Some(node)), extNode.next, remainingKey, op)
134+
} else Some(op(acc, None))
137135

138136
case branch: BranchNode =>
139-
if (searchKey.isEmpty) Some(op(acc, node))
137+
if (searchKey.isEmpty) Some(op(acc, Some(node)))
140138
else
141139
pathTraverse(
142-
op(acc, node),
140+
op(acc, Some(node)),
143141
branch.children(searchKey(0)),
144142
searchKey.slice(1, searchKey.length),
145143
op
@@ -149,13 +147,13 @@ class MerklePatriciaTrie[K, V] private (private[mpt] val rootNode: Option[MptNod
149147
pathTraverse(acc, getFromHash(bytes, nodeStorage), searchKey, op)
150148

151149
case NullNode =>
152-
None
150+
Some(op(acc, None))
153151
}
154152
}
155153

156154
rootNode match {
157155
case Some(root) =>
158-
pathTraverse(acc, root, searchKey, op)
156+
pathTraverse(op(acc, Some(root)), root, searchKey, op)
159157
case None =>
160158
None
161159
}

src/test/scala/io/iohk/ethereum/ObjectGenerators.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,10 @@ trait ObjectGenerators {
5050
} yield (aByteList.toArray, t)
5151
}
5252

53-
def keyValueListGen(): Gen[List[(Int, Int)]] = {
53+
def keyValueListGen(minValue: Int = Int.MinValue, maxValue: Int = Int.MaxValue): Gen[List[(Int, Int)]] = {
5454
for {
55-
aKeyList <- Gen.nonEmptyListOf(Arbitrary.arbitrary[Int]).map(_.distinct)
55+
values <- Gen.chooseNum(minValue, maxValue)
56+
aKeyList <- Gen.nonEmptyListOf(values).map(_.distinct)
5657
} yield aKeyList.zip(aKeyList)
5758
}
5859

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

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import io.iohk.ethereum.consensus.blocks.CheckpointBlockGenerator
66
import io.iohk.ethereum.db.dataSource.EphemDataSource
77
import io.iohk.ethereum.db.storage.StateStorage
88
import io.iohk.ethereum.domain.BlockHeader.HeaderExtraFields.HefPostEcip1097
9-
import io.iohk.ethereum.mpt.MerklePatriciaTrie
9+
import io.iohk.ethereum.mpt.{HashNode, MerklePatriciaTrie}
1010
import io.iohk.ethereum.{BlockHelpers, Fixtures, ObjectGenerators}
1111
import io.iohk.ethereum.ObjectGenerators._
1212
import io.iohk.ethereum.proof.MptProofVerifier
@@ -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)
@@ -162,6 +165,26 @@ class BlockchainSpec extends AnyFlatSpec with Matchers with ScalaCheckPropertyCh
162165
}
163166
}
164167

168+
it should "return proof for non-existent account" in new EphemBlockchainTestSetup {
169+
val emptyMpt = MerklePatriciaTrie[Address, Account](
170+
storagesInstance.storages.stateStorage.getBackingStorage(0)
171+
)
172+
val mptWithAcc = emptyMpt.put(Address(42), Account.empty(UInt256(7)))
173+
174+
val headerWithAcc = Fixtures.Blocks.ValidBlock.header.copy(stateRoot = ByteString(mptWithAcc.getRootHash))
175+
176+
blockchain.storeBlockHeader(headerWithAcc).commit()
177+
178+
val wrongAddress = Address(666)
179+
val retrievedAccountProofWrong = blockchain.getAccountProof(wrongAddress, headerWithAcc.number)
180+
//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(root node) that we iterated
181+
(retrievedAccountProofWrong.getOrElse(Vector.empty).toList match {
182+
case _ @HashNode(_) :: Nil => true
183+
case _ => false
184+
}) shouldBe true
185+
mptWithAcc.get(wrongAddress) shouldBe None
186+
}
187+
165188
it should "return correct best block number after applying and rollbacking blocks" in new TestSetup {
166189
forAll(intGen(min = 1: Int, max = maxNumberBlocksToImport)) { numberBlocksToImport =>
167190
val testSetup = newSetup()

0 commit comments

Comments
 (0)