Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/etcm 533 non membership proof #899

Merged
merged 10 commits into from
Jan 27, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package io.iohk.ethereum.txExecTest.util

import java.time.Clock
import java.util.concurrent.atomic.AtomicReference

import akka.actor.ActorSystem
import akka.util.ByteString
import com.typesafe.config.ConfigFactory
Expand All @@ -15,6 +14,7 @@ import io.iohk.ethereum.db.storage.pruning.{ArchivePruning, PruningMode}
import io.iohk.ethereum.db.storage.{AppStateStorage, StateStorage}
import io.iohk.ethereum.domain.BlockHeader.HeaderExtraFields.HefEmpty
import io.iohk.ethereum.domain.{Blockchain, UInt256, _}
import io.iohk.ethereum.jsonrpc.ProofService.{EmptyStorageValueProof, StorageProof, StorageProofKey, StorageValueProof}
import io.iohk.ethereum.ledger.{InMemoryWorldStateProxy, InMemoryWorldStateProxyStorage}
import io.iohk.ethereum.mpt.MptNode
import io.iohk.ethereum.network.EtcPeerManagerActor.PeerInfo
Expand Down Expand Up @@ -150,7 +150,7 @@ class BlockchainMock(genesisHash: ByteString) extends Blockchain {
rootHash: NodeHash,
position: BigInt,
ethCompatibleStorage: Boolean
): Option[(BigInt, Seq[MptNode])] = None
): StorageProof = EmptyStorageValueProof(StorageProofKey(position))

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

Expand Down
19 changes: 12 additions & 7 deletions src/main/scala/io/iohk/ethereum/domain/Blockchain.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.iohk.ethereum.domain

import java.util.concurrent.atomic.AtomicReference

import akka.util.ByteString
import cats.syntax.flatMap._
import cats.instances.option._
Expand All @@ -13,6 +12,7 @@ import io.iohk.ethereum.db.storage._
import io.iohk.ethereum.db.storage.pruning.PruningMode
import io.iohk.ethereum.domain
import io.iohk.ethereum.domain.BlockchainImpl.BestBlockLatestCheckpointNumbers
import io.iohk.ethereum.jsonrpc.ProofService.StorageProof
import io.iohk.ethereum.ledger.{InMemoryWorldStateProxy, InMemoryWorldStateProxyStorage}
import io.iohk.ethereum.mpt.{MerklePatriciaTrie, MptNode}
import io.iohk.ethereum.utils.{ByteStringUtils, Logger}
Expand Down Expand Up @@ -95,11 +95,17 @@ trait Blockchain {
*/
def getAccountStorageAt(rootHash: ByteString, position: BigInt, ethCompatibleStorage: Boolean): ByteString

/**
* Get a storage-value and its proof being the path from the root node until the last matching node.
*
* @param rootHash storage root hash
* @param position storage position
*/
def getStorageProofAt(
rootHash: ByteString,
position: BigInt,
ethCompatibleStorage: Boolean
): Option[(BigInt, Seq[MptNode])]
): StorageProof

/**
* Returns the receipts based on a block hash
Expand Down Expand Up @@ -307,16 +313,15 @@ class BlockchainImpl(
rootHash: ByteString,
position: BigInt,
ethCompatibleStorage: Boolean
): Option[(BigInt, Seq[MptNode])] = {
): StorageProof = {
val storage: MptStorage = stateStorage.getBackingStorage(0)
val mpt: MerklePatriciaTrie[BigInt, BigInt] = {
if (ethCompatibleStorage) domain.EthereumUInt256Mpt.storageMpt(rootHash, storage)
else domain.ArbitraryIntegerMpt.storageMpt(rootHash, storage)
}
for {
value <- mpt.get(position)
proof <- mpt.getProof(position)
} yield (value, proof)
val value: Option[BigInt] = mpt.get(position)
val proof: Option[Vector[MptNode]] = mpt.getProof(position)
StorageProof(position, value, proof)
}

private def persistBestBlocksData(): Unit = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import io.iohk.ethereum.jsonrpc.JsonRpcError.InvalidParams
import io.iohk.ethereum.jsonrpc.ProofService.{GetProofRequest, GetProofResponse, StorageProofKey}
import io.iohk.ethereum.jsonrpc.serialization.JsonEncoder
import io.iohk.ethereum.jsonrpc.serialization.JsonMethodDecoder
import org.json4s.Extraction
import org.json4s.JsonAST.{JArray, JString, JValue, _}
import org.json4s.JsonDSL._

object EthProofJsonMethodsImplicits extends JsonMethodsImplicits {
def extractStorageKeys(input: JValue): Either[JsonRpcError, Seq[StorageProofKey]] = {
Expand Down
60 changes: 41 additions & 19 deletions src/main/scala/io/iohk/ethereum/jsonrpc/EthProofService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import akka.util.ByteString
import cats.implicits._
import io.iohk.ethereum.consensus.blocks.BlockGenerator
import io.iohk.ethereum.domain.{Account, Address, Block, Blockchain, UInt256}
import io.iohk.ethereum.jsonrpc.ProofService.StorageProof.asRlpSerializedNode
import io.iohk.ethereum.jsonrpc.ProofService.{
GetProofRequest,
GetProofResponse,
ProofAccount,
StorageProof,
StorageProofKey
StorageProofKey,
StorageValueProof
}
import io.iohk.ethereum.mpt.{MptNode, MptTraversals}
import monix.eval.Task
Expand All @@ -30,8 +32,26 @@ object ProofService {

case class GetProofResponse(proofAccount: ProofAccount)

/** The key used to get the storage slot in its account tree */
case class StorageProofKey(v: BigInt) extends AnyVal
sealed trait StorageProof {
def key: StorageProofKey
def value: BigInt
def proof: Seq[ByteString]
}

object StorageProof {
def apply(position: BigInt, value: Option[BigInt], proof: Option[Vector[MptNode]]): StorageProof =
(value, proof) match {
case (Some(value), Some(proof)) =>
StorageValueProof(StorageProofKey(position), value, proof.map(asRlpSerializedNode))
case (None, Some(proof)) =>
EmptyStorageValue(StorageProofKey(position), proof.map(asRlpSerializedNode))
case (Some(value), None) => EmptyStorageProof(StorageProofKey(position), value)
case (None, None) => EmptyStorageValueProof(StorageProofKey(position))
}

def asRlpSerializedNode(node: MptNode): ByteString =
ByteString(MptTraversals.encodeNode(node))
}

/**
* Object proving a relationship of a storage value to an account's storageHash
Expand All @@ -40,11 +60,20 @@ object ProofService {
* @param value the value of the storage slot in its account tree
* @param proof the set of node values needed to traverse a patricia merkle tree (from root to leaf) to retrieve a value
*/
case class StorageProof(
key: StorageProofKey,
value: BigInt,
proof: Seq[ByteString]
)
case class EmptyStorageValueProof(key: StorageProofKey) extends StorageProof {
val value: BigInt = BigInt(0)
val proof: Seq[ByteString] = Seq.empty[MptNode].map(asRlpSerializedNode)
}
case class EmptyStorageValue(key: StorageProofKey, proof: Seq[ByteString]) extends StorageProof {
val value: BigInt = BigInt(0)
}
case class EmptyStorageProof(key: StorageProofKey, value: BigInt) extends StorageProof {
val proof: Seq[ByteString] = Seq.empty[MptNode].map(asRlpSerializedNode)
}
case class StorageValueProof(key: StorageProofKey, value: BigInt, proof: Seq[ByteString]) extends StorageProof

/** The key used to get the storage slot in its account tree */
case class StorageProofKey(v: BigInt) extends AnyVal

/**
* The merkle proofs of the specified account connecting them to the blockhash of the block specified.
Expand Down Expand Up @@ -143,14 +172,14 @@ class EthProofService(blockchain: Blockchain, blockGenerator: BlockGenerator, et
blockchain.getAccountProof(address, blockNumber).map(_.map(asRlpSerializedNode)),
noAccountProof(address, blockNumber)
)
storageProof <- getStorageProof(account, storageKeys)
storageProof = getStorageProof(account, storageKeys)
} yield ProofAccount(account, accountProof, storageProof, address)
}

def getStorageProof(
account: Account,
storageKeys: Seq[StorageProofKey]
): Either[JsonRpcError, Seq[StorageProof]] = {
): Seq[StorageProof] = {
storageKeys.toList
.map { storageKey =>
blockchain
Expand All @@ -159,21 +188,14 @@ class EthProofService(blockchain: Blockchain, blockGenerator: BlockGenerator, et
position = storageKey.v,
ethCompatibleStorage = ethCompatibleStorage
)
.map { case (value, proof) => StorageProof(storageKey, value, proof.map(asRlpSerializedNode)) }
.toRight(noStorageProof(account, storageKey))
}
.sequence
.map(_.toSeq)
}

private def noStorageProof(account: Account, storagekey: StorageProofKey): JsonRpcError =
JsonRpcError.LogicError(s"No storage proof for [${account.toString}] storage key [${storagekey.toString}]")

private def noAccount(address: Address, blockNumber: BigInt): JsonRpcError =
JsonRpcError.LogicError(s"No storage proof for Address [${address.toString}] blockNumber [${blockNumber.toString}]")
JsonRpcError.LogicError(s"No account found for Address [${address.toString}] blockNumber [${blockNumber.toString}]")

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

private def asRlpSerializedNode(node: MptNode): ByteString =
ByteString(MptTraversals.encodeNode(node))
Expand Down
36 changes: 17 additions & 19 deletions src/main/scala/io/iohk/ethereum/mpt/MerklePatriciaTrie.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import io.iohk.ethereum.rlp.RLPImplicits._
import io.iohk.ethereum.rlp.{encode => encodeRLP}
import org.bouncycastle.util.encoders.Hex
import io.iohk.ethereum.utils.ByteUtils.matchingLength

import scala.annotation.tailrec

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

case BranchNode(_, terminator, _, _, _) =>
terminator.map(term => vSerializer.fromBytes(term.toArray[Byte]))
case (_, Some(BranchNode(_, terminator, _, _, _))) =>
terminator.map(term => vSerializer.fromBytes(term.toArray[Byte]))

case _ => None
}
case _ => None
}.flatten
}

Expand All @@ -105,7 +102,8 @@ class MerklePatriciaTrie[K, V] private (private[mpt] val rootNode: Option[MptNod
def getProof(key: K): Option[Vector[MptNode]] = {
pathTraverse[Vector[MptNode]](Vector.empty, mkKeyNibbles(key)) { case (acc, node) =>
node match {
case nextNodeOnExt @ (_: BranchNode | _: ExtensionNode | _: LeafNode) => acc :+ nextNodeOnExt
case Some(nextNodeOnExt @ (_: BranchNode | _: ExtensionNode | _: LeafNode | _: HashNode)) =>
acc :+ nextNodeOnExt
case _ => acc
}
}
Expand All @@ -121,25 +119,25 @@ class MerklePatriciaTrie[K, V] private (private[mpt] val rootNode: Option[MptNod
* @tparam T accumulator type
* @return accumulated data or None if key doesn't exist
*/
private def pathTraverse[T](acc: T, searchKey: Array[Byte])(op: (T, MptNode) => T): Option[T] = {
private def pathTraverse[T](acc: T, searchKey: Array[Byte])(op: (T, Option[MptNode]) => T): Option[T] = {
dzajkowski marked this conversation as resolved.
Show resolved Hide resolved

@tailrec
def pathTraverse(acc: T, node: MptNode, searchKey: Array[Byte], op: (T, MptNode) => T): Option[T] = {
def pathTraverse(acc: T, node: MptNode, searchKey: Array[Byte], op: (T, Option[MptNode]) => T): Option[T] = {
node match {
case LeafNode(key, _, _, _, _) =>
if (key.toArray[Byte] sameElements searchKey) Some(op(acc, node)) else None
if (key.toArray[Byte] sameElements searchKey) Some(op(acc, Some(node))) else Some(op(acc, None))

case extNode @ ExtensionNode(sharedKey, _, _, _, _) =>
val (commonKey, remainingKey) = searchKey.splitAt(sharedKey.length)
if (searchKey.length >= sharedKey.length && (sharedKey.toArray[Byte] sameElements commonKey)) {
pathTraverse(op(acc, node), extNode.next, remainingKey, op)
} else None
pathTraverse(op(acc, Some(node)), extNode.next, remainingKey, op)
} else Some(op(acc, None))

case branch: BranchNode =>
if (searchKey.isEmpty) Some(op(acc, node))
if (searchKey.isEmpty) Some(op(acc, Some(node)))
else
pathTraverse(
op(acc, node),
op(acc, Some(node)),
branch.children(searchKey(0)),
searchKey.slice(1, searchKey.length),
op
Expand All @@ -149,13 +147,13 @@ class MerklePatriciaTrie[K, V] private (private[mpt] val rootNode: Option[MptNod
pathTraverse(acc, getFromHash(bytes, nodeStorage), searchKey, op)

case NullNode =>
None
Some(op(acc, None))
}
}

rootNode match {
case Some(root) =>
pathTraverse(acc, root, searchKey, op)
pathTraverse(op(acc, Some(root)), root, searchKey, op)
case None =>
None
}
Expand Down
5 changes: 3 additions & 2 deletions src/test/scala/io/iohk/ethereum/ObjectGenerators.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ trait ObjectGenerators {
} yield (aByteList.toArray, t)
}

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

Expand Down
27 changes: 25 additions & 2 deletions src/test/scala/io/iohk/ethereum/domain/BlockchainSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import io.iohk.ethereum.consensus.blocks.CheckpointBlockGenerator
import io.iohk.ethereum.db.dataSource.EphemDataSource
import io.iohk.ethereum.db.storage.StateStorage
import io.iohk.ethereum.domain.BlockHeader.HeaderExtraFields.HefPostEcip1097
import io.iohk.ethereum.mpt.MerklePatriciaTrie
import io.iohk.ethereum.mpt.{HashNode, MerklePatriciaTrie}
import io.iohk.ethereum.{BlockHelpers, Fixtures, ObjectGenerators}
import io.iohk.ethereum.ObjectGenerators._
import io.iohk.ethereum.proof.MptProofVerifier
Expand Down Expand Up @@ -152,7 +152,10 @@ class BlockchainSpec extends AnyFlatSpec with Matchers with ScalaCheckPropertyCh
//unhappy path
val wrongAddress = Address(666)
val retrievedAccountProofWrong = blockchain.getAccountProof(wrongAddress, headerWithAcc.number)
retrievedAccountProofWrong.isDefined shouldBe false
//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
retrievedAccountProofWrong.isDefined shouldBe true
retrievedAccountProofWrong.size shouldBe 1
mptWithAcc.get(wrongAddress) shouldBe None
AnastasiiaL marked this conversation as resolved.
Show resolved Hide resolved

//happy path
val retrievedAccountProof = blockchain.getAccountProof(address, headerWithAcc.number)
Expand All @@ -162,6 +165,26 @@ class BlockchainSpec extends AnyFlatSpec with Matchers with ScalaCheckPropertyCh
}
}

it should "return proof for non-existent account" in new EphemBlockchainTestSetup {
val emptyMpt = MerklePatriciaTrie[Address, Account](
storagesInstance.storages.stateStorage.getBackingStorage(0)
)
val mptWithAcc = emptyMpt.put(Address(42), Account.empty(UInt256(7)))

val headerWithAcc = Fixtures.Blocks.ValidBlock.header.copy(stateRoot = ByteString(mptWithAcc.getRootHash))

blockchain.storeBlockHeader(headerWithAcc).commit()

val wrongAddress = Address(666)
val retrievedAccountProofWrong = blockchain.getAccountProof(wrongAddress, headerWithAcc.number)
//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
(retrievedAccountProofWrong.getOrElse(Vector.empty).toList match {
case _ @HashNode(_) :: Nil => true
case _ => false
}) shouldBe true
mptWithAcc.get(wrongAddress) shouldBe None
}

it should "return correct best block number after applying and rollbacking blocks" in new TestSetup {
forAll(intGen(min = 1: Int, max = maxNumberBlocksToImport)) { numberBlocksToImport =>
val testSetup = newSetup()
Expand Down
Loading