From c138a4913c40ceab93029bf2a387557f4726e6da Mon Sep 17 00:00:00 2001 From: KonradStaniec Date: Fri, 9 Sep 2022 15:59:36 +0200 Subject: [PATCH] Implement getStorageAt endpoint (#1216) * Implement getStorageAt endpoint --- lc_proxy/rpc/rpc_eth_lc_api.nim | 50 ++++++++-- lc_proxy/tests/test_proof_validation.nim | 36 +++++++ lc_proxy/validate_proof.nim | 120 ++++++++++++++++++++--- vendor/nimbus-eth2 | 2 +- 4 files changed, 185 insertions(+), 23 deletions(-) diff --git a/lc_proxy/rpc/rpc_eth_lc_api.nim b/lc_proxy/rpc/rpc_eth_lc_api.nim index 536eb8b78..df2992a83 100644 --- a/lc_proxy/rpc/rpc_eth_lc_api.nim +++ b/lc_proxy/rpc/rpc_eth_lc_api.nim @@ -9,6 +9,7 @@ import stint, + stew/byteutils, chronicles, json_rpc/[rpcserver, rpcclient], web3, @@ -25,6 +26,10 @@ logScope: template encodeQuantity(value: UInt256): HexQuantityStr = hexQuantityStr("0x" & value.toHex()) + +template encodeHexData(value: UInt256): HexDataStr = + hexDataStr("0x" & toBytesBe(value).toHex) + template encodeQuantity(value: Quantity): HexQuantityStr = hexQuantityStr(encodeQuantity(value.uint64)) @@ -33,6 +38,20 @@ type LightClientRpcProxy* = ref object server*: RpcHttpServer executionPayload*: Opt[ExecutionPayloadV1] + +template checkPreconditions(payload: Opt[ExecutionPayloadV1], quantityTag: string) = + if payload.isNone(): + raise newException(ValueError, "Syncing") + + if quantityTag != "latest": + # TODO: for now we support only latest block, as its semantically most straight + # forward, i.e it is last received and a valid ExecutionPayloadV1. + # Ultimately we could keep track of n last valid payloads and support number + # queries for this set of blocks. + # `Pending` could be mapped to some optimistic header with the block + # fetched on demand. + raise newException(ValueError, "Only latest block is supported") + proc installEthApiHandlers*(lcProxy: LightClientRpcProxy) = template payload(): Opt[ExecutionPayloadV1] = lcProxy.executionPayload @@ -45,16 +64,7 @@ proc installEthApiHandlers*(lcProxy: LightClientRpcProxy) = # TODO quantity tag should be better typed lcProxy.server.rpc("eth_getBalance") do(address: Address, quantityTag: string) -> HexQuantityStr: - if payload.isNone: - raise newException(ValueError, "Syncing") - - if quantityTag != "latest": - # TODO for now we support only latest block, as its semanticly most streight - # forward i.e it is last received and valid ExecutionPayloadV1. - # Ultimatly we could keep track of n last valid payload and support number - # queries for this set of blocks - # `Pending` coud be mapped to some optimisc header with block fetched on demand - raise newException(ValueError, "Only latest block is supported") + checkPreconditions(payload, quantityTag) # When requesting state for `latest` block number, we need to translate # `latest` to actual block number as `latest` on proxy and on data provider @@ -82,3 +92,23 @@ proc installEthApiHandlers*(lcProxy: LightClientRpcProxy) = return encodeQuantity(proof.balance) else: raise newException(ValueError, "Data provided by data provider server is invalid") + + lcProxy.server.rpc("eth_getStorageAt") do(address: Address, slot: HexDataStr, quantityTag: string) -> HexDataStr: + checkPreconditions(payload, quantityTag) + + let + executionPayload = payload.get + uslot = UInt256.fromHex(slot.string) + blockNumber = executionPayload.blockNumber.uint64 + + info "Forwarding eth_getStorageAt", executionBn = blockNumber + + let proof = await lcProxy.client.eth_getProof(address, @[uslot], blockId(blockNumber)) + + let dataResult = getStorageData(executionPayload.stateRoot, uslot, proof) + + if dataResult.isOk(): + let slotValue = dataResult.get() + return encodeHexData(slotValue) + else: + raise newException(ValueError, dataResult.error) diff --git a/lc_proxy/tests/test_proof_validation.nim b/lc_proxy/tests/test_proof_validation.nim index daf7afa5b..9c852c808 100644 --- a/lc_proxy/tests/test_proof_validation.nim +++ b/lc_proxy/tests/test_proof_validation.nim @@ -47,3 +47,39 @@ suite "Merkle proof of inclusion validation": storageRoot, rlpNodes ) + + test "Validate storage proof": + let slotValue = UInt256.fromHex("0x25a92a5853702f199bb2d805bba05d67025214a8") + let stateRoot = FixedBytes[32].fromHex("0x99b31961b56190853d6f20b17298c8e3aed0a88860f80d4662fb41712f93491d") + let proof = ProofResponse( + address: Address(hexToByteArray[20]("0x805fe47d1fe7d86496753bb4b36206953c1ae660")), + accountProof: @[ + RlpEncodedBytes(hexToSeqByte("0xf90211a07399cef9c0d815f0ea8c485255564f7f749d3352f4cd13590b6354d521243433a0786151335ec66b81e6a5f8b0750d40a1440894febfae9021a246af0011dddc39a0d8be7a536ea6f5a70b6684a6bc5a954a3d87b424fe9c8d31e48b7697c2a439d6a0f270896eacce7a14d5f73c30c8d9864e18a7499a8c941872d0f543a583eed8d7a03d19b1d9c3294de123bab76b580db62e847b007ad24d7627c39513ad63ce7edea0cae2f92408fda1e856f7acb53c8f26984cc783dbb9ac8c4e93af362f458c2f0ca0fb6e6b3e8878fcd181be3b09eab73334d8ef6d89e26babf8072ba32e1ae5ce4aa016d11cb1bac71884c5057ec8afe714aa9b72125fb9ec6e2d9481869641a8e026a00d9828dddc21d4c4b5457a233ac518f508c88b3cd4e814eac2e28a2e2a632a08a00d396f8c462121f2e06a4288d668e98aafd2821d99b6dd1887aa660775318875a045c2a82a655fff0f7ab212635fff00ebf40e6918423f32c8abfeeb588a23c7c7a08be08722cc8bbd898101ce8a5299145d0964fd50eaf923f3feeb7df414c35fe5a0697c454102347fbd17fce54dc165220abac4ee992f1e19791c55ac17e14518e6a09502fa4ea94b14920ad984e6ab4d837068c5f00cfaefd1787a454a4705b4e2d5a08b9b53e865cd8910964ddfc3a0f1d87d8ea665ee9c0813d564de3744ffb147d7a0ae674b410e770b9fd691c72252a67ae461c82b91b88a9b42f1bbcc4a8733585780")), + RlpEncodedBytes(hexToSeqByte("0xf90211a09412f363cb562e21fccd9b4bb47a3b61215421eb9770b4a5730cb2cb6ea5bbc9a0ec0c28e23f6a9310c216965c08e1ec9c9fc9ec6112f12100263003be31d76e34a0378e652ba0a437830411206a126853271d599951a45d94a267ad5e31516563cca09b3a79cc886fd7dce88c60d39d8280ee625725b21e066358b8ca8c768911490ba088abe98c021c60a356abab43474f3fd9a3a852161b7933b0c8c932ba47e900cda052a527e30350d47b10cf22f43cf1ab288d8ffb723e6de54bc8f83b6189db2266a0bd5d883c317ab84c75785444ff61feae3ed9c2d4aab2017637ca8e974806e3c7a006c3451ace02de15b65cfd7c19c02c5c1040335f995dd957d3e07e8dc3079485a015af1df6ff5da1c38d32cf7a788c9764556e149c09bcfa92f020f77b155a03b4a0e0a5c318b7881c9e9a56d7f652a3532d5003565d5bd94337a180ce4ff1db8467a030d718ee799736a89d72b69189f1e27b0563d500ace86729685d056b09c6701ca0e2d4a13abc46bbe7b34290ca6ebd94dbbe228460fb6bb10413f6144459aa8463a0a7af172aa05889771cea29cdebdcd6bb7d4a6ec09b96629bbb64a99f4856fab8a07ece8ac878f91890f51a66f9ab6a145e1a6734cd2505ce8f2be7548617b065fda0c8819fca92b6ff1e3ea5624c0b4b582554ef7696a0e96fe696858739a8a5af9da0314d236293a35e001b071f1682bdc1e20c79af444b115fd0f8e697803b727d9180")), + RlpEncodedBytes(hexToSeqByte("0xf90211a0e8dd0264185a25c11978679ef7f80c8e50804286b5f3f15ac26d11f8f4fe8612a0398a219de5df7abe3f16eb8571583d262196726e74575ad86b88e2e27a14c529a0aafdffb23bdb02384873ed6d5df2beaf8ba1a3d7eb89a7a9cf145861ef3fd18fa09f7299d9c05587feee543d34f49c59401997d6a9d44e78d7a144b002e838be7ea0bc385eca8be25dcdf3b6b58a230ef917da28e568dbff4ad05b054feec92bd53da0bea9a90577955f6fa5a60150f37b9e77a6b233e0b75168cfb3aec7d8eb098163a0ca0114d558ad84b5afd4f5a0bd5f7469758a446cffbed94c996e4c57302d9b0aa0158a3d1058957bc5cd41d816bdb42a066132cde9439b7c1925af42256cfcfc07a0ec12e2d93e3f7862e5c86de6131caae8ac623ddbc9109b74ebc62a5ed919f60aa0fd900e1d33be45abd3e1ddda1447739c2eb90e3b53604a16c65b44bd96b2a999a019693693106b6a90707e1c9bdad704dc128c8a5690c126d573daaf007f2aedcba0ce308e3364196afb1988196382015c90e8d65f349b72e60ced504870c7f307d3a0677e417e01c738c8818f647984dbfb7683820dd8a50dba02eeb79f4314f6d81ba0ecd210d3a979b55b6d33ebc53434441d3cade0e96d6a101a082bda6cd4ca88baa055d90842c7606222257aa1e98d949a23f7d676512bb21395bfacebc9d718e1bda027e473f101976ee9201de7f867158e165eedf3fe472567a9178d96ca6856c01c80")), + RlpEncodedBytes(hexToSeqByte("0xf90211a09044017f2c2743fb2a70885262ce105c4d63095cc85e998b844ca5a08e2a6e14a020f6c529f0fcd791436640ba14ec77555cf06ac65cb75bd15a70270c940ad900a0c8f51f52d62882314e3b004ca0c3afdca1cb374582e484b10c371f553ff951c4a0975183fc3697fe81104fd4acc5e3a6167614786efcc52c940187cd517dc5f88fa00bf7e6698b3f1ba0963ced4ecd5f3fce6b7ae9346ee331ddfe54323a9027f944a06405c7db333d93ebad9e660b513373c974c5492012401a8ae14b5f2953c80e90a0bf9bad639ffcc1a8e047e03aace972039aa25c463c5880d1fd8907f684f82440a02cca4a3426ceabb8da2f06fe3491147a01eb0134128ec174d6836902f76ca9f2a070a886a555112f301b1545f883ed686519ae56cae46fb512e93986c37c63ec29a07df58e619bfcb18dc3ac5b49cf9eaa2817616e81e59204ccc66fd063b35aa4c6a09185c8c8e02e8a5dcfe642e776cf691208d0c79f8886585fc1bad1deebdf1dc8a0b660d946ab0ccca5467484c88789bc0baefcbb8f36b091f5758da2aa8f46a5e8a0127150d53d1b8d5aa81eeb898af87dbc55a2d0c2c6f1ef4d575e2dc1138dffa4a0a50a7266499bd407a2e60475df45b14cea1673957aca1a03740a5606117aa2bfa0434e10c0ff2e6b5ac8713a40b29bf8995e302c7d0b821c55c9f0cfc6d81fce87a0ed37d722b70122d0a5c96cd52810781b8b1a88277064d691a282d896fd301f0a80")), + RlpEncodedBytes(hexToSeqByte("0xf90211a05febd97d485ae84c4aa444320eddf9361b45ac0cd966eca4e86acadbbb9b1919a06b014b81f4328a4c92bd6639a0b7f06e55a875b5891b958e3f32aed237a60128a0878844a864828586bad11cc05427f942475e040da74a17affa41284c7c2b8a5ca0afa3cf559f694ca29166fd02462f8dc4263b25f5a6bcd012b5ea7792adf2aebba02ce34320a3ade650b5d6951fe0c6628a03ce227f06e46e2646f0dc85cb227348a01b4b6baed79a52ed6737deffe81b0f2fc55f2888639470266222946b9c85d47fa014b15187e49585497a789fa11b3499603a250f0f48f8f12313b453937239af72a0a304eac356264043417a7c9ca624d92ff08d32d3390d229a3ed96c089ac0a602a0f723b00e76886ba3206a92fa098996c87990e4231d4f08667201cc0891b778a7a0d67ede626d7500ad5d4f11bd9b9de562209627c50f3cfd967a03b41a17009a1aa04f46efe2492e2905d02802a7ff1f5d1718edfe36fcae46dd45954ee9861f13e1a042d8c5e5834755e7183313443996885b561b1ba6daa0e484b60d0becc245460da07302dec93846a62ecb50892edbc5a485775700aebf816badfa42af6e3853d16aa04b2b25079c39ad8d517a61e47141519b4b497b36c4edb81e6f34c7917823d414a01aac7bb66afad0b0e7f8ffeb928d34ab59fe428e4a77b30689956fabb94e9f56a01accc5cd467129ec35675c04625ae805dc974fef16119f49e44f27853aee58f480")), + RlpEncodedBytes(hexToSeqByte("0xf90111a0d7559f6dc085b66ea2cf7928f3aff9ca07e9518113f44678a662b8fd5c91ab60a0b584675ea78138a4fd465b08895f3e791e717c2e259bff313d922994ad1085da80a0901123278214a37f6cd1d18d67c9ad8c51a64f8cf18c7de342d59a6b072d2f25a097533fa6c344fb668de71a3aa0c30da9190d2b6ce2974b05ba6caa9c8a9c495aa0344b048b149b2c08d1aa92ba1741009c6a8a4726bf5e7db12c78f4ad521122ae80808080a092677d6f81cdcafb80add68e79d7322af8771830d7d444a7587e8296203b16a88080a0930e22b8279f903ce0046cabc3cd24f2659dcb9c08980509ee0d12cc0ddecbee80a0156aa8328726c06af81bc6219dfada3e7826f1337da178258886015e94b0501c80")), + RlpEncodedBytes(hexToSeqByte("0xf8679e20da5951bceaed385c03546f45f276b83d86bc0755e045f92f1d9071f431b846f8440180a07bb85da974b0ee4efcb379f528bcf7e947a55901d5a2c0d38bc9cc16c851e785a03b45ab254ec24f2bcb75a922f15031796bc433ea5a4514783705d185321e5f82")) + ], + balance: UInt256.fromHex("0x0"), + codeHash: FixedBytes[32].fromHex("0x3b45ab254ec24f2bcb75a922f15031796bc433ea5a4514783705d185321e5f82"), + nonce: Quantity(uint64(1)), + storageHash: FixedBytes[32].fromHex("0x7bb85da974b0ee4efcb379f528bcf7e947a55901d5a2c0d38bc9cc16c851e785"), + storageProof: @[ + StorageProof( + key: u256(0), + value: slotValue, + proof: @[ + RlpEncodedBytes(hexToSeqByte("0xf8b18080a0a24d07e12c3fedf892f5325b16f489fc7f14d267cef99aa6b7b74da7eab7a47a80a078a183d317824c686d46f3cf0a344cd5e1f8f73e095ea29864d3f8ef599dc643808080a0925cf9d2d4b35a02377f6ef9c51f1bbab4842dff4befd4ee5d85bbc15bb8554a808080a020db4b8930daa709db04119c07ee39896da3ac30b961b7901a7bea81a5329a8680a024f50ff7b56d6a9de8a2fcfe21e1710377dddb106c39c834362bd41a9bce22518080")), + RlpEncodedBytes(hexToSeqByte("0xf7a0390decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563959425a92a5853702f199bb2d805bba05d67025214a8")) + ] + ) + ] + ) + + let validationResult = getStorageData(stateRoot, u256(0), proof) + + check: + validationResult.isOk() + validationResult.get == slotValue diff --git a/lc_proxy/validate_proof.nim b/lc_proxy/validate_proof.nim index 4f42b816a..c632fc97c 100644 --- a/lc_proxy/validate_proof.nim +++ b/lc_proxy/validate_proof.nim @@ -8,18 +8,33 @@ {.push raises: [Defect].} import - std/[sequtils, typetraits], + std/[sequtils, typetraits, options], stint, + stew/results, eth/common/eth_types as etypes, eth/common/eth_types_rlp, eth/rlp, eth/trie/hexary, web3/ethtypes +export results + func toMDigest(arg: FixedBytes[32]): MDigest[256] = MDigest[256](data: distinctBase(arg)) -proc isAccountProofValid*( +proc isValidProof( + branch: seq[seq[byte]], + rootHash: KeccakHash, + key, value: seq[byte]): bool = + try: + # TODO: Investigate if this handles proof of non-existence. + # Probably not as bool is not expressive enough to say if proof is valid, but + # key actually does not exists in MPT + return isValidBranch(branch, rootHash, key, value) + except RlpError: + return false + +proc isAccountProofValidInternal( stateRoot: FixedBytes[32], accountAddress: Address, accountBalance: UInt256, @@ -27,7 +42,7 @@ proc isAccountProofValid*( accountCodeHash: CodeHash, accountStorageRootHash: StorageHash, mptNodes: seq[RlpEncodedBytes] - ): bool = + ): Option[etypes.Account] = let mptNodesBytes = mptNodes.mapIt(distinctBase(it)) keccakStateRootHash = toMDigest(stateRoot) @@ -40,13 +55,94 @@ proc isAccountProofValid*( accountEncoded = rlp.encode(acc) accountKey = toSeq(keccakHash(distinctBase(accountAddress)).data) - try: - return isValidBranch( - mptNodesBytes, - keccakStateRootHash, - accountKey, - accountEncoded - ) - except RlpError: - return false + let validProof = isValidProof( + mptNodesBytes, + keccakStateRootHash, + accountKey, + accountEncoded + ) + if validProof: + return some(acc) + else: + return none(etypes.Account) + +proc isAccountProofValid*( + stateRoot: FixedBytes[32], + accountAddress: Address, + accountBalance: UInt256, + accountNonce: Quantity, + accountCodeHash: CodeHash, + accountStorageRootHash: StorageHash, + mptNodes: seq[RlpEncodedBytes] + ): bool = + + let maybeAccount = isAccountProofValidInternal( + stateRoot, + accountAddress, + accountBalance, + accountNonce, + accountCodeHash, + accountStorageRootHash, + mptNodes + ) + + return maybeAccount.isSome() + +proc isStorageProofValid( + account: etypes.Account, + storageProof: StorageProof): bool = + let + storageMptNodes = storageProof.proof.mapIt(distinctBase(it)) + key = toSeq(keccakHash(toBytesBE(storageProof.key)).data) + encodedValue = rlp.encode(storageProof.value) + + return isValidProof(storageMptNodes, account.storageRoot, key, encodedValue) + +proc getStorageData*( + stateRoot: FixedBytes[32], + requestedSlot: UInt256, + proof: ProofResponse): Result[UInt256, string] = + + let maybeAccount = isAccountProofValidInternal( + stateRoot, + proof.address, + proof.balance, + proof.nonce, + proof.codeHash, + proof.storageHash, + proof.accountProof + ) + + if maybeAccount.isSome(): + let account = maybeAccount.unsafeGet() + + if account.storageRoot == etypes.EMPTY_ROOT_HASH: + # valid account with empty storage, in that case getStorageAt + # return 0 value + return ok(u256(0)) + + if len(proof.storageProof) != 1: + return err("no storage proof for requested slot") + + let sproof = proof.storageProof[0] + + if len(sproof.proof) == 0: + return err("empty mpt proof for account with not empty storage") + + if sproof.key != requestedSlot: + return err("received proof for invalid slot") + + if sproof.value == UInt256.zero: + # TODO: zero value means that storage is empty. We need to verify proof of + # no existance. As we currenctly do not have that ability just, return zero + # value + return ok(sproof.value) + + if isStorageProofValid(account, sproof): + return ok(sproof.value) + else: + return err("invalid storage proof") + + else: + return err("invalid account proof") diff --git a/vendor/nimbus-eth2 b/vendor/nimbus-eth2 index d9ceb61db..a770fadd0 160000 --- a/vendor/nimbus-eth2 +++ b/vendor/nimbus-eth2 @@ -1 +1 @@ -Subproject commit d9ceb61dbd9e9763ba073b564eac7478e072f367 +Subproject commit a770fadd013c11c174eb9c37ddeccc2ceb5411d4