Skip to content

Commit 99491c7

Browse files
author
Piotr Paradziński
authored
[ETCM-468] add JSON-RPC method eth-getProof (#869)
[ETCM-468] add eth_getProof JSON-RPC endpoint (without non-membership proofs)
1 parent 3c9842a commit 99491c7

18 files changed

+721
-37
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,14 @@ class BlockchainMock(genesisHash: ByteString) extends Blockchain {
144144
override lazy val hash: ByteString = genesisHash
145145
}
146146

147+
override def getAccountProof(address: Address, blockNumber: BigInt): Option[Vector[MptNode]] = None
148+
149+
override def getStorageProofAt(
150+
rootHash: NodeHash,
151+
position: BigInt,
152+
ethCompatibleStorage: Boolean
153+
): Option[(BigInt, Seq[MptNode])] = None
154+
147155
override protected def getHashByBlockNumber(number: BigInt): Option[ByteString] = Some(genesisHash)
148156

149157
override def getBlockHeaderByHash(hash: ByteString): Option[BlockHeader] = Some(new FakeHeader())

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

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package io.iohk.ethereum.domain
33
import java.util.concurrent.atomic.AtomicReference
44

55
import akka.util.ByteString
6+
import cats.syntax.flatMap._
7+
import cats.instances.option._
68
import io.iohk.ethereum.db.dataSource.DataSourceBatchUpdate
79
import io.iohk.ethereum.db.dataSource.RocksDbDataSource.IterationError
810
import io.iohk.ethereum.db.storage.NodeStorage.{NodeEncoded, NodeHash}
@@ -83,6 +85,8 @@ trait Blockchain {
8385
*/
8486
def getAccount(address: Address, blockNumber: BigInt): Option[Account]
8587

88+
def getAccountProof(address: Address, blockNumber: BigInt): Option[Vector[MptNode]]
89+
8690
/**
8791
* Get account storage at given position
8892
*
@@ -91,6 +95,12 @@ trait Blockchain {
9195
*/
9296
def getAccountStorageAt(rootHash: ByteString, position: BigInt, ethCompatibleStorage: Boolean): ByteString
9397

98+
def getStorageProofAt(
99+
rootHash: ByteString,
100+
position: BigInt,
101+
ethCompatibleStorage: Boolean
102+
): Option[(BigInt, Seq[MptNode])]
103+
94104
/**
95105
* Returns the receipts based on a block hash
96106
* @param blockhash
@@ -267,13 +277,18 @@ class BlockchainImpl(
267277
}
268278

269279
override def getAccount(address: Address, blockNumber: BigInt): Option[Account] =
270-
getBlockHeaderByNumber(blockNumber).flatMap { bh =>
280+
getAccountMpt(blockNumber) >>= (_.get(address))
281+
282+
override def getAccountProof(address: Address, blockNumber: BigInt): Option[Vector[MptNode]] =
283+
getAccountMpt(blockNumber) >>= (_.getProof(address))
284+
285+
private def getAccountMpt(blockNumber: BigInt): Option[MerklePatriciaTrie[Address, Account]] =
286+
getBlockHeaderByNumber(blockNumber).map { bh =>
271287
val storage = stateStorage.getBackingStorage(blockNumber)
272-
val mpt = MerklePatriciaTrie[Address, Account](
273-
bh.stateRoot.toArray,
274-
storage
288+
MerklePatriciaTrie[Address, Account](
289+
rootHash = bh.stateRoot.toArray,
290+
source = storage
275291
)
276-
mpt.get(address)
277292
}
278293

279294
override def getAccountStorageAt(
@@ -288,6 +303,22 @@ class BlockchainImpl(
288303
ByteString(mpt.get(position).getOrElse(BigInt(0)).toByteArray)
289304
}
290305

306+
override def getStorageProofAt(
307+
rootHash: ByteString,
308+
position: BigInt,
309+
ethCompatibleStorage: Boolean
310+
): Option[(BigInt, Seq[MptNode])] = {
311+
val storage: MptStorage = stateStorage.getBackingStorage(0)
312+
val mpt: MerklePatriciaTrie[BigInt, BigInt] = {
313+
if (ethCompatibleStorage) domain.EthereumUInt256Mpt.storageMpt(rootHash, storage)
314+
else domain.ArbitraryIntegerMpt.storageMpt(rootHash, storage)
315+
}
316+
for {
317+
value <- mpt.get(position)
318+
proof <- mpt.getProof(position)
319+
} yield (value, proof)
320+
}
321+
291322
private def persistBestBlocksData(): Unit = {
292323
val currentBestBlockNumber = getBestBlockNumber()
293324
val currentBestCheckpointNumber = getLatestCheckpointBlockNumber()

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import akka.util.ByteString
44
import io.iohk.ethereum.jsonrpc.EthService._
55
import io.iohk.ethereum.jsonrpc.JsonRpcError.InvalidParams
66
import io.iohk.ethereum.jsonrpc.PersonalService.{SendTransactionRequest, SendTransactionResponse, SignRequest}
7+
import io.iohk.ethereum.jsonrpc.ProofService.{GetProofRequest, GetProofResponse, StorageProofKey}
78
import io.iohk.ethereum.jsonrpc.serialization.JsonEncoder.OptionToNull._
89
import io.iohk.ethereum.jsonrpc.serialization.JsonMethodDecoder.NoParamsMethodDecoder
910
import io.iohk.ethereum.jsonrpc.serialization.{JsonEncoder, JsonMethodCodec, JsonMethodDecoder}
@@ -601,4 +602,49 @@ object EthJsonMethodsImplicits extends JsonMethodsImplicits {
601602

602603
def encodeJson(t: GetStorageRootResponse): JValue = encodeAsHex(t.storageRoot)
603604
}
605+
606+
def extractStorageKeys(input: JValue): Either[JsonRpcError, Seq[StorageProofKey]] = {
607+
import cats.syntax.traverse._
608+
import cats.syntax.either._
609+
input match {
610+
case JArray(elems) =>
611+
elems.traverse { x =>
612+
extractQuantity(x)
613+
.map(StorageProofKey.apply)
614+
.leftMap(_ => InvalidParams(s"Invalid param storage proof key: $x"))
615+
}
616+
case _ => Left(InvalidParams())
617+
}
618+
}
619+
620+
implicit val eth_getProof: JsonMethodDecoder[GetProofRequest] with JsonEncoder[GetProofResponse] =
621+
new JsonMethodDecoder[GetProofRequest] with JsonEncoder[GetProofResponse] {
622+
override def decodeJson(params: Option[JArray]): Either[JsonRpcError, GetProofRequest] =
623+
params match {
624+
case Some(JArray((address: JString) :: storageKeys :: (blockNumber: JValue) :: Nil)) =>
625+
for {
626+
addressParsed <- extractAddress(address)
627+
storageKeysParsed <- extractStorageKeys(storageKeys)
628+
blockNumberParsed <- extractBlockParam(blockNumber)
629+
} yield GetProofRequest(addressParsed, storageKeysParsed, blockNumberParsed)
630+
case _ => Left(InvalidParams())
631+
}
632+
633+
override def encodeJson(t: GetProofResponse): JValue = {
634+
JObject(
635+
"accountProof" -> JArray(t.proofAccount.accountProof.toList.map { ap => encodeAsHex(ap) }),
636+
"balance" -> encodeAsHex(t.proofAccount.balance),
637+
"codeHash" -> encodeAsHex(t.proofAccount.codeHash),
638+
"nonce" -> encodeAsHex(t.proofAccount.nonce),
639+
"storageHash" -> encodeAsHex(t.proofAccount.storageHash),
640+
"storageProof" -> JArray(t.proofAccount.storageProof.toList.map { sp =>
641+
JObject(
642+
"key" -> encodeAsHex(sp.key.v),
643+
"proof" -> JArray(sp.proof.toList.map { p => encodeAsHex(p) }),
644+
"value" -> encodeAsHex(sp.value)
645+
)
646+
})
647+
)
648+
}
649+
}
604650
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package io.iohk.ethereum.jsonrpc
2+
3+
import akka.util.ByteString
4+
import cats.implicits._
5+
import io.iohk.ethereum.consensus.blocks.BlockGenerator
6+
import io.iohk.ethereum.domain.{Account, Address, Block, Blockchain, UInt256}
7+
import io.iohk.ethereum.jsonrpc.EthService._
8+
import io.iohk.ethereum.jsonrpc.ProofService.{
9+
GetProofRequest,
10+
GetProofResponse,
11+
ProofAccount,
12+
StorageProof,
13+
StorageProofKey
14+
}
15+
import io.iohk.ethereum.mpt.{MptNode, MptTraversals}
16+
import monix.eval.Task
17+
18+
object ProofService {
19+
20+
/**
21+
* Request to eth get proof
22+
*
23+
* @param address the address of the account or contract
24+
* @param storageKeys array of storage keys;
25+
* a storage key is indexed from the solidity compiler by the order it is declared.
26+
* For mappings it uses the keccak of the mapping key with its position (and recursively for X-dimensional mappings).
27+
* See eth_getStorageAt
28+
* @param blockNumber block number (integer block number or string "latest", "earliest", ...)
29+
*/
30+
case class GetProofRequest(address: Address, storageKeys: Seq[StorageProofKey], blockNumber: BlockParam)
31+
32+
case class GetProofResponse(proofAccount: ProofAccount)
33+
34+
/** The key used to get the storage slot in its account tree */
35+
case class StorageProofKey(v: BigInt) extends AnyVal
36+
37+
/**
38+
* Object proving a relationship of a storage value to an account's storageHash
39+
*
40+
* @param key storage proof key
41+
* @param value the value of the storage slot in its account tree
42+
* @param proof the set of node values needed to traverse a patricia merkle tree (from root to leaf) to retrieve a value
43+
*/
44+
case class StorageProof(
45+
key: StorageProofKey,
46+
value: BigInt,
47+
proof: Seq[ByteString]
48+
)
49+
50+
/**
51+
* The merkle proofs of the specified account connecting them to the blockhash of the block specified.
52+
*
53+
* Proof of account consists of:
54+
* - account object: nonce, balance, storageHash, codeHash
55+
* - Markle Proof for the account starting with stateRoot from specified block
56+
* - Markle Proof for each requested storage entry starting with a storage Hash from the account
57+
*
58+
* @param address the address of the account or contract of the request
59+
* @param accountProof Markle Proof for the account starting with stateRoot from specified block
60+
* @param balance the Ether balance of the account or contract of the request
61+
* @param codeHash the code hash of the contract of the request (keccak(NULL) if external account)
62+
* @param nonce the transaction count of the account or contract of the request
63+
* @param storageHash the storage hash of the contract of the request (keccak(rlp(NULL)) if external account)
64+
* @param storageProof current block header PoW hash
65+
*/
66+
case class ProofAccount(
67+
address: Address,
68+
accountProof: Seq[ByteString],
69+
balance: BigInt,
70+
codeHash: ByteString,
71+
nonce: UInt256,
72+
storageHash: ByteString,
73+
storageProof: Seq[StorageProof]
74+
)
75+
76+
object ProofAccount {
77+
78+
def apply(
79+
account: Account,
80+
accountProof: Seq[ByteString],
81+
storageProof: Seq[StorageProof],
82+
address: Address
83+
): ProofAccount =
84+
ProofAccount(
85+
address = address,
86+
accountProof = accountProof,
87+
balance = account.balance,
88+
codeHash = account.codeHash,
89+
nonce = account.nonce,
90+
storageHash = account.storageRoot,
91+
storageProof = storageProof
92+
)
93+
}
94+
95+
sealed trait MptProofError
96+
object MptProofError {
97+
case object UnableRebuildMpt extends MptProofError
98+
case object KeyNotFoundInRebuidMpt extends MptProofError
99+
}
100+
}
101+
102+
trait ProofService {
103+
104+
/**
105+
* Returns the account- and storage-values of the specified account including the Merkle-proof.
106+
*/
107+
def getProof(req: GetProofRequest): ServiceResponse[GetProofResponse]
108+
}
109+
110+
/**
111+
* Spec: [EIP-1186](https://eips.ethereum.org/EIPS/eip-1186)
112+
* besu: https://github.com/PegaSysEng/pantheon/pull/1824/files
113+
* parity: https://github.com/openethereum/parity-ethereum/pull/9001
114+
* geth: https://github.com/ethereum/go-ethereum/pull/17737
115+
*/
116+
class EthProofService(blockchain: Blockchain, blockGenerator: BlockGenerator, ethCompatibleStorage: Boolean)
117+
extends ProofService {
118+
119+
def getProof(req: GetProofRequest): ServiceResponse[GetProofResponse] = {
120+
getProofAccount(req.address, req.storageKeys, req.blockNumber)
121+
.map(_.map(GetProofResponse.apply))
122+
}
123+
124+
/**
125+
* Get account and storage values for account including Merkle Proof.
126+
*
127+
* @param address address of the account
128+
* @param storageKeys storage keys which should be proofed and included
129+
* @param block block number or string "latest", "earliest"
130+
* @return
131+
*/
132+
def getProofAccount(
133+
address: Address,
134+
storageKeys: Seq[StorageProofKey],
135+
block: BlockParam
136+
): Task[Either[JsonRpcError, ProofAccount]] = Task {
137+
for {
138+
blockNumber <- resolveBlock(block).map(_.block.number)
139+
account <- Either.fromOption(
140+
blockchain.getAccount(address, blockNumber),
141+
noAccount(address, blockNumber)
142+
)
143+
accountProof <- Either.fromOption(
144+
blockchain.getAccountProof(address, blockNumber).map(_.map(asRlpSerializedNode)),
145+
noAccountProof(address, blockNumber)
146+
)
147+
storageProof <- getStorageProof(account, storageKeys)
148+
} yield ProofAccount(account, accountProof, storageProof, address)
149+
}
150+
151+
def getStorageProof(
152+
account: Account,
153+
storageKeys: Seq[StorageProofKey]
154+
): Either[JsonRpcError, Seq[StorageProof]] = {
155+
storageKeys.toList
156+
.map { storageKey =>
157+
blockchain
158+
.getStorageProofAt(
159+
rootHash = account.storageRoot,
160+
position = storageKey.v,
161+
ethCompatibleStorage = ethCompatibleStorage
162+
)
163+
.map { case (value, proof) => StorageProof(storageKey, value, proof.map(asRlpSerializedNode)) }
164+
.toRight(noStorageProof(account, storageKey))
165+
}
166+
.sequence
167+
.map(_.toSeq)
168+
}
169+
170+
private def noStorageProof(account: Account, storagekey: StorageProofKey): JsonRpcError =
171+
JsonRpcError.LogicError(s"No storage proof for [${account.toString}] storage key [${storagekey.toString}]")
172+
173+
private def noAccount(address: Address, blockNumber: BigInt): JsonRpcError =
174+
JsonRpcError.LogicError(s"No storage proof for Address [${address.toString}] blockNumber [${blockNumber.toString}]")
175+
176+
private def noAccountProof(address: Address, blockNumber: BigInt): JsonRpcError =
177+
JsonRpcError.LogicError(s"No storage proof for Address [${address.toString}] blockNumber [${blockNumber.toString}]")
178+
179+
private def asRlpSerializedNode(node: MptNode): ByteString =
180+
ByteString(MptTraversals.encodeNode(node))
181+
182+
private def resolveBlock(blockParam: BlockParam): Either[JsonRpcError, ResolvedBlock] = {
183+
def getBlock(number: BigInt): Either[JsonRpcError, Block] = {
184+
blockchain
185+
.getBlockByNumber(number)
186+
.toRight(JsonRpcError.InvalidParams(s"Block $number not found"))
187+
}
188+
189+
blockParam match {
190+
case BlockParam.WithNumber(blockNumber) => getBlock(blockNumber).map(ResolvedBlock(_, pendingState = None))
191+
case BlockParam.Earliest => getBlock(0).map(ResolvedBlock(_, pendingState = None))
192+
case BlockParam.Latest => getBlock(blockchain.getBestBlockNumber()).map(ResolvedBlock(_, pendingState = None))
193+
case BlockParam.Pending =>
194+
blockGenerator.getPendingBlockAndState
195+
.map(pb => ResolvedBlock(pb.pendingBlock.block, pendingState = Some(pb.worldState)))
196+
.map(Right.apply)
197+
.getOrElse(resolveBlock(BlockParam.Latest)) //Default behavior in other clients
198+
}
199+
}
200+
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -862,8 +862,7 @@ class EthService(
862862
def getBlock(number: BigInt): Either[JsonRpcError, Block] = {
863863
blockchain
864864
.getBlockByNumber(number)
865-
.map(Right.apply)
866-
.getOrElse(Left(JsonRpcError.InvalidParams(s"Block $number not found")))
865+
.toRight(JsonRpcError.InvalidParams(s"Block $number not found"))
867866
}
868867

869868
blockParam match {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import io.iohk.ethereum.jsonrpc.EthService._
66
import io.iohk.ethereum.jsonrpc.MantisService.{GetAccountTransactionsRequest, GetAccountTransactionsResponse}
77
import io.iohk.ethereum.jsonrpc.NetService._
88
import io.iohk.ethereum.jsonrpc.PersonalService._
9+
import io.iohk.ethereum.jsonrpc.ProofService.{GetProofRequest, GetProofResponse}
910
import io.iohk.ethereum.jsonrpc.QAService.{
1011
GenerateCheckpointRequest,
1112
GenerateCheckpointResponse,
@@ -31,6 +32,7 @@ class JsonRpcController(
3132
qaService: QAService,
3233
checkpointingService: CheckpointingService,
3334
mantisService: MantisService,
35+
proofService: ProofService,
3436
override val config: JsonRpcConfig
3537
) extends ApisBuilder
3638
with Logger
@@ -199,6 +201,8 @@ class JsonRpcController(
199201
)
200202
case req @ JsonRpcRequest(_, "eth_pendingTransactions", _, _) =>
201203
handle[EthPendingTransactionsRequest, EthPendingTransactionsResponse](ethService.ethPendingTransactions, req)
204+
case req @ JsonRpcRequest(_, "eth_getProof", _, _) =>
205+
handle[GetProofRequest, GetProofResponse](proofService.getProof, req)
202206
}
203207

204208
private def handleDebugRequest: PartialFunction[JsonRpcRequest, Task[JsonRpcResponse]] = {

0 commit comments

Comments
 (0)