|
| 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 | +} |
0 commit comments