From 372ec98586706f5d3d9aff5ab4c5516283e12e8d Mon Sep 17 00:00:00 2001 From: Makoto Inoue <2630+makoto@users.noreply.github.com> Date: Mon, 6 May 2024 15:45:30 +0100 Subject: [PATCH] Make scroll verifier working --- arb-gateway/src/worker.ts | 2 +- arb-verifier/contracts/ArbVerifier.sol | 11 +- evm-verifier/contracts/EVMProofHelper.sol | 72 ++++----- evm-verifier/contracts/SecureMerkleTrie.sol | 44 ++++++ op-gateway/src/worker.ts | 2 +- op-verifier/contracts/OPVerifier.sol | 4 +- scroll-verifier/contracts/EVMProofHelper2.sol | 143 ------------------ scroll-verifier/contracts/ScrollVerifier.sol | 46 +++--- ..._arb_verifier.ts => 00_scroll_verifier.ts} | 0 9 files changed, 112 insertions(+), 212 deletions(-) delete mode 100644 scroll-verifier/contracts/EVMProofHelper2.sol rename scroll-verifier/deploy_l1/{00_arb_verifier.ts => 00_scroll_verifier.ts} (100%) diff --git a/arb-gateway/src/worker.ts b/arb-gateway/src/worker.ts index 30276b80..5c1fb73b 100644 --- a/arb-gateway/src/worker.ts +++ b/arb-gateway/src/worker.ts @@ -57,7 +57,7 @@ async function fetch(request: CFWRequest, env: Env) { await tracker.trackEvent(request, 'request', { props }, true); return app .handle(request) - .then(tracker.logResult.bind(null, propsDecoder, request)); + .then(tracker.logResult.bind(tracker, propsDecoder, request)); } export default { diff --git a/arb-verifier/contracts/ArbVerifier.sol b/arb-verifier/contracts/ArbVerifier.sol index a868a628..541a79e8 100644 --- a/arb-verifier/contracts/ArbVerifier.sol +++ b/arb-verifier/contracts/ArbVerifier.sol @@ -4,6 +4,7 @@ import {StateProof, EVMProofHelper} from '@ensdomains/evm-verifier/contracts/EVM import {IEVMVerifier} from '@ensdomains/evm-verifier/contracts/IEVMVerifier.sol'; import {Node, IRollupCore} from '@arbitrum/nitro-contracts/src/rollup/IRollupCore.sol'; import {RLPReader} from '@eth-optimism/contracts-bedrock/src/libraries/rlp/RLPReader.sol'; +import {SecureMerkleTrie} from '@ensdomains/evm-verifier/contracts/SecureMerkleTrie.sol'; struct ArbWitnessData { bytes32 version; @@ -63,14 +64,8 @@ contract ArbVerifier is IEVMVerifier { //Now that we know that the block is valid, we can get the state root from the block. bytes32 stateRoot = getStateRootFromBlock(arbData.rlpEncodedBlock); - - values = EVMProofHelper.getStorageValues( - target, - commands, - constants, - stateRoot, - stateProof - ); + bytes32 storageRoot = SecureMerkleTrie.getStorageRoot(stateRoot, target, stateProof.stateTrieWitness); + return EVMProofHelper.getStorageValues(target, SecureMerkleTrie.getTrieProof, commands, constants, storageRoot, stateProof.storageProofs); } /* diff --git a/evm-verifier/contracts/EVMProofHelper.sol b/evm-verifier/contracts/EVMProofHelper.sol index d91f1396..c691862a 100644 --- a/evm-verifier/contracts/EVMProofHelper.sol +++ b/evm-verifier/contracts/EVMProofHelper.sol @@ -21,48 +21,35 @@ library EVMProofHelper { error UnknownOpcode(uint8); error InvalidSlotSize(uint256 size); - /** - * @notice Get the storage root for the provided merkle proof - * @param stateRoot The state root the witness was generated against - * @param target The address we are fetching a storage root for - * @param witness A witness proving the value of the storage root for `target`. - * @return The storage root retrieved from the provided state root - */ - function getStorageRoot(bytes32 stateRoot, address target, bytes[] memory witness) private pure returns (bytes32) { - (bool exists, bytes memory encodedResolverAccount) = SecureMerkleTrie.get( - abi.encodePacked(target), - witness, - stateRoot - ); - if(!exists) { - revert AccountNotFound(target); - } - RLPReader.RLPItem[] memory accountState = RLPReader.readList(encodedResolverAccount); - return bytes32(RLPReader.readBytes(accountState[2])); - } - /** * @notice Prove whether the provided storage slot is part of the storageRoot + * @param getter function to verify the storage proof * @param storageRoot the storage root for the account that contains the storage slot * @param slot The storage key we are fetching the value of * @param witness the StorageProof struct containing the necessary proof data * @return The retrieved storage proof value or 0x if the storage slot is empty */ - function getSingleStorageProof(bytes32 storageRoot, uint256 slot, bytes[] memory witness) private pure returns (bytes memory) { - (bool exists, bytes memory retrievedValue) = SecureMerkleTrie.get( - abi.encodePacked(slot), + function getSingleStorageProof( + address target, + function(address,uint256,bytes[] memory, bytes32) internal view returns(bytes memory) getter, + bytes32 storageRoot, + uint256 slot, + bytes[] memory witness + ) private view returns (bytes memory) { + return getter( + target, + slot, witness, storageRoot ); - if(!exists) { - // Nonexistent values are treated as zero. - return ""; - } - return RLPReader.readBytes(retrievedValue); } - function getFixedValue(bytes32 storageRoot, uint256 slot, bytes[] memory witness) private pure returns(bytes32) { - bytes memory value = getSingleStorageProof(storageRoot, slot, witness); + function getFixedValue( + address target, + function(address,uint256,bytes[] memory, bytes32) internal view returns(bytes memory) getter, + bytes32 storageRoot, uint256 slot, bytes[] memory witness + ) private view returns(bytes32) { + bytes memory value = getSingleStorageProof(target, getter, storageRoot, slot, witness); // RLP encoded storage slots are stored without leading 0 bytes. // Casting to bytes32 appends trailing 0 bytes, so we have to bit shift to get the // original fixed-length representation back. @@ -82,7 +69,7 @@ library EVMProofHelper { } } - function computeFirstSlot(bytes32 command, bytes[] memory constants, bytes[] memory values) private pure returns(bool isDynamic, uint256 slot) { + function computeFirstSlot(bytes32 command, bytes[] memory constants, bytes[] memory values) internal pure returns(bool isDynamic, uint256 slot) { uint8 flags = uint8(command[0]); isDynamic = (flags & FLAG_DYNAMIC) != 0; @@ -96,8 +83,12 @@ library EVMProofHelper { } } - function getDynamicValue(bytes32 storageRoot, uint256 slot, StateProof memory proof, uint256 proofIdx) private pure returns(bytes memory value, uint256 newProofIdx) { - uint256 firstValue = uint256(getFixedValue(storageRoot, slot, proof.storageProofs[proofIdx++])); + function getDynamicValue( + address target, + function(address,uint256,bytes[] memory, bytes32) internal view returns(bytes memory) getter, + bytes32 storageRoot, uint256 slot, bytes[][] memory proof, uint256 proofIdx) private view returns(bytes memory value, uint256 newProofIdx + ) { + uint256 firstValue = uint256(getFixedValue(target, getter,storageRoot, slot, proof[proofIdx++])); if(firstValue & 0x01 == 0x01) { // Long value: first slot is `length * 2 + 1`, following slots are data. uint256 length = (firstValue - 1) / 2; @@ -107,10 +98,10 @@ library EVMProofHelper { // all at once, but we're trying to avoid writing new library code. while(length > 0) { if(length < 32) { - value = bytes.concat(value, getSingleStorageProof(storageRoot, slot++, proof.storageProofs[proofIdx++]).slice(0, length)); + value = bytes.concat(value, getSingleStorageProof(target, getter, storageRoot, slot++, proof[proofIdx++]).slice(0, length)); length = 0; } else { - value = bytes.concat(value, getSingleStorageProof(storageRoot, slot++, proof.storageProofs[proofIdx++])); + value = bytes.concat(value, getSingleStorageProof(target, getter, storageRoot, slot++, proof[proofIdx++])); length -= 32; } } @@ -122,20 +113,23 @@ library EVMProofHelper { } } - function getStorageValues(address target, bytes32[] memory commands, bytes[] memory constants, bytes32 stateRoot, StateProof memory proof) internal pure returns(bytes[] memory values) { - bytes32 storageRoot = getStorageRoot(stateRoot, target, proof.stateTrieWitness); + function getStorageValues( + address target, + function(address,uint256,bytes[] memory, bytes32) internal view returns(bytes memory) getter, + bytes32[] memory commands, bytes[] memory constants, bytes32 storageRoot, bytes[][] memory proof) internal view returns(bytes[] memory values + ) { uint256 proofIdx = 0; values = new bytes[](commands.length); for(uint256 i = 0; i < commands.length; i++) { bytes32 command = commands[i]; (bool isDynamic, uint256 slot) = computeFirstSlot(command, constants, values); if(!isDynamic) { - values[i] = abi.encode(getFixedValue(storageRoot, slot, proof.storageProofs[proofIdx++])); + values[i] = abi.encode(getFixedValue(target, getter, storageRoot, slot, proof[proofIdx++])); if(values[i].length > 32) { revert InvalidSlotSize(values[i].length); } } else { - (values[i], proofIdx) = getDynamicValue(storageRoot, slot, proof, proofIdx); + (values[i], proofIdx) = getDynamicValue(target, getter, storageRoot, slot, proof, proofIdx); } } } diff --git a/evm-verifier/contracts/SecureMerkleTrie.sol b/evm-verifier/contracts/SecureMerkleTrie.sol index 40211f05..64cfb10f 100644 --- a/evm-verifier/contracts/SecureMerkleTrie.sol +++ b/evm-verifier/contracts/SecureMerkleTrie.sol @@ -7,6 +7,7 @@ pragma solidity ^0.8.0; /* Library Imports */ import { MerkleTrie } from "./MerkleTrie.sol"; +import {RLPReader} from '@eth-optimism/contracts-bedrock/src/libraries/rlp/RLPReader.sol'; /** * @title SecureMerkleTrie @@ -14,6 +15,49 @@ import { MerkleTrie } from "./MerkleTrie.sol"; * keys. Ethereum's state trie hashes input keys before storing them. */ library SecureMerkleTrie { + error AccountNotFound(address); + + /* + * @notice Get the storage value for the provided merkle proof + * @param target The address we are fetching a storage root for + * @param witness A witness proving the value of the storage root for `target`. + * @param root The state root the witness was generated against + * @return The storage value + */ + + function getTrieProof(address, uint256 slot, bytes[] memory witness, bytes32 root) internal pure returns(bytes memory){ + (bool exists, bytes memory retrievedValue) = get( + abi.encodePacked(slot), + witness, + root + ); + if(!exists) { + // Nonexistent values are treated as zero. + return ""; + } + return RLPReader.readBytes(retrievedValue); + } + + /** + * @notice Get the storage root for the provided merkle proof + * @param stateRoot The state root the witness was generated against + * @param target The address we are fetching a storage root for + * @param witness A witness proving the value of the storage root for `target`. + * @return The storage root retrieved from the provided state root + */ + function getStorageRoot(bytes32 stateRoot, address target, bytes[] memory witness) internal view returns (bytes32) { + (bool exists, bytes memory encodedResolverAccount) = get( + abi.encodePacked(target), + witness, + stateRoot + ); + if(!exists) { + revert AccountNotFound(target); + } + RLPReader.RLPItem[] memory accountState = RLPReader.readList(encodedResolverAccount); + return bytes32(RLPReader.readBytes(accountState[2])); + } + /** * @notice Verifies a proof that a given key/value pair is present in the Merkle trie. * diff --git a/op-gateway/src/worker.ts b/op-gateway/src/worker.ts index daac73f9..12719b88 100644 --- a/op-gateway/src/worker.ts +++ b/op-gateway/src/worker.ts @@ -58,7 +58,7 @@ async function fetch(request: CFWRequest, env: Env) { await tracker.trackEvent(request, 'request', { props }, true); return app .handle(request) - .then(tracker.logResult.bind(null, propsDecoder, request)); + .then(tracker.logResult.bind(tracker, propsDecoder, request)); } export default { diff --git a/op-verifier/contracts/OPVerifier.sol b/op-verifier/contracts/OPVerifier.sol index c0fab2db..3dac8c70 100644 --- a/op-verifier/contracts/OPVerifier.sol +++ b/op-verifier/contracts/OPVerifier.sol @@ -6,6 +6,7 @@ import { RLPReader } from "@eth-optimism/contracts-bedrock/src/libraries/rlp/RLP import { StateProof, EVMProofHelper } from "@ensdomains/evm-verifier/contracts/EVMProofHelper.sol"; import { Types } from "@eth-optimism/contracts-bedrock/src/libraries/Types.sol"; import { Hashing } from "@eth-optimism/contracts-bedrock/src/libraries/Hashing.sol"; +import {SecureMerkleTrie} from '@ensdomains/evm-verifier/contracts/SecureMerkleTrie.sol'; struct OPWitnessData { uint256 l2OutputIndex; @@ -38,6 +39,7 @@ contract OPVerifier is IEVMVerifier { if(l2out.outputRoot != expectedRoot) { revert OutputRootMismatch(opData.l2OutputIndex, expectedRoot, l2out.outputRoot); } - return EVMProofHelper.getStorageValues(target, commands, constants, opData.outputRootProof.stateRoot, stateProof); + bytes32 storageRoot = SecureMerkleTrie.getStorageRoot(opData.outputRootProof.stateRoot, target, stateProof.stateTrieWitness); + return EVMProofHelper.getStorageValues(target, SecureMerkleTrie.getTrieProof, commands, constants, storageRoot, stateProof.storageProofs); } } diff --git a/scroll-verifier/contracts/EVMProofHelper2.sol b/scroll-verifier/contracts/EVMProofHelper2.sol deleted file mode 100644 index fb37d074..00000000 --- a/scroll-verifier/contracts/EVMProofHelper2.sol +++ /dev/null @@ -1,143 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -import {RLPReader} from "@eth-optimism/contracts-bedrock/src/libraries/rlp/RLPReader.sol"; -import {Bytes} from "@eth-optimism/contracts-bedrock/src/libraries/Bytes.sol"; -// import {SecureMerkleTrie} from "./SecureMerkleTrie.sol"; -import {SecureMerkleTrie} from '@ensdomains/evm-verifier/contracts/SecureMerkleTrie.sol'; - -struct StateProof { - bytes[] stateTrieWitness; // Witness proving the `storageRoot` against a state root. - bytes[][] storageProofs; // An array of proofs of individual storage elements -} - -uint8 constant OP_CONSTANT = 0x00; -uint8 constant OP_BACKREF = 0x20; -uint8 constant FLAG_DYNAMIC = 0x01; - -library EVMProofHelper2 { - using Bytes for bytes; - - error AccountNotFound(address); - error UnknownOpcode(uint8); - error InvalidSlotSize(uint256 size); - - /** - * @notice Get the storage root for the provided merkle proof - * @param stateRoot The state root the witness was generated against - * @param target The address we are fetching a storage root for - * @param witness A witness proving the value of the storage root for `target`. - * @return The storage root retrieved from the provided state root - */ - function getStorageRoot(bytes32 stateRoot, address target, bytes[] memory witness) private pure returns (bytes32) { - (bool exists, bytes memory encodedResolverAccount) = SecureMerkleTrie.get( - abi.encodePacked(target), - witness, - stateRoot - ); - if(!exists) { - revert AccountNotFound(target); - } - RLPReader.RLPItem[] memory accountState = RLPReader.readList(encodedResolverAccount); - return bytes32(RLPReader.readBytes(accountState[2])); - } - - /** - * @notice Prove whether the provided storage slot is part of the storageRoot - * @param storageRoot the storage root for the account that contains the storage slot - * @param slot The storage key we are fetching the value of - * @param witness the StorageProof struct containing the necessary proof data - * @return The retrieved storage proof value or 0x if the storage slot is empty - */ - function getSingleStorageProof(bytes32 storageRoot, uint256 slot, bytes[] memory witness) private pure returns (bytes memory) { - (bool exists, bytes memory retrievedValue) = SecureMerkleTrie.get( - abi.encodePacked(slot), - witness, - storageRoot - ); - if(!exists) { - // Nonexistent values are treated as zero. - return ""; - } - return RLPReader.readBytes(retrievedValue); - } - - function getFixedValue(bytes32 storageRoot, uint256 slot, bytes[] memory witness) private pure returns(bytes32) { - bytes memory value = getSingleStorageProof(storageRoot, slot, witness); - // RLP encoded storage slots are stored without leading 0 bytes. - // Casting to bytes32 appends trailing 0 bytes, so we have to bit shift to get the - // original fixed-length representation back. - return bytes32(value) >> (256 - 8 * value.length); - } - - function executeOperation(bytes1 operation, bytes[] memory constants, bytes[] memory values) private pure returns(bytes memory) { - uint8 opcode = uint8(operation) & 0xe0; - uint8 operand = uint8(operation) & 0x1f; - - if(opcode == OP_CONSTANT) { - return constants[operand]; - } else if(opcode == OP_BACKREF) { - return values[operand]; - } else { - revert UnknownOpcode(opcode); - } - } - - function computeFirstSlot(bytes32 command, bytes[] memory constants, bytes[] memory values) internal pure returns(bool isDynamic, uint256 slot) { - uint8 flags = uint8(command[0]); - isDynamic = (flags & FLAG_DYNAMIC) != 0; - - bytes memory slotData = executeOperation(command[1], constants, values); - require(slotData.length == 32, "First path element must be 32 bytes"); - slot = uint256(bytes32(slotData)); - - for(uint256 j = 2; j < 32 && command[j] != 0xff; j++) { - bytes memory index = executeOperation(command[j], constants, values); - slot = uint256(keccak256(abi.encodePacked(index, slot))); - } - } - - function getDynamicValue(bytes32 storageRoot, uint256 slot, StateProof memory proof, uint256 proofIdx) private pure returns(bytes memory value, uint256 newProofIdx) { - uint256 firstValue = uint256(getFixedValue(storageRoot, slot, proof.storageProofs[proofIdx++])); - if(firstValue & 0x01 == 0x01) { - // Long value: first slot is `length * 2 + 1`, following slots are data. - uint256 length = (firstValue - 1) / 2; - value = ""; - slot = uint256(keccak256(abi.encodePacked(slot))); - // This is horribly inefficient - O(n^2). A better approach would be to build an array of words and concatenate them - // all at once, but we're trying to avoid writing new library code. - while(length > 0) { - if(length < 32) { - value = bytes.concat(value, getSingleStorageProof(storageRoot, slot++, proof.storageProofs[proofIdx++]).slice(0, length)); - length = 0; - } else { - value = bytes.concat(value, getSingleStorageProof(storageRoot, slot++, proof.storageProofs[proofIdx++])); - length -= 32; - } - } - return (value, proofIdx); - } else { - // Short value: least significant byte is `length * 2`, other bytes are data. - uint256 length = (firstValue & 0xFF) / 2; - return (abi.encode(firstValue).slice(0, length), proofIdx); - } - } - - function getStorageValues(address target, bytes32[] memory commands, bytes[] memory constants, bytes32 stateRoot, StateProof memory proof) internal pure returns(bytes[] memory values) { - bytes32 storageRoot = getStorageRoot(stateRoot, target, proof.stateTrieWitness); - uint256 proofIdx = 0; - values = new bytes[](commands.length); - for(uint256 i = 0; i < commands.length; i++) { - bytes32 command = commands[i]; - (bool isDynamic, uint256 slot) = computeFirstSlot(command, constants, values); - if(!isDynamic) { - values[i] = abi.encode(getFixedValue(storageRoot, slot, proof.storageProofs[proofIdx++])); - if(values[i].length > 32) { - revert InvalidSlotSize(values[i].length); - } - } else { - (values[i], proofIdx) = getDynamicValue(storageRoot, slot, proof, proofIdx); - } - } - } -} \ No newline at end of file diff --git a/scroll-verifier/contracts/ScrollVerifier.sol b/scroll-verifier/contracts/ScrollVerifier.sol index 4388df22..1f1f727c 100644 --- a/scroll-verifier/contracts/ScrollVerifier.sol +++ b/scroll-verifier/contracts/ScrollVerifier.sol @@ -1,8 +1,15 @@ //SPDX-License-Identifier: MIT pragma solidity ^0.8.17; import {StateProof, EVMProofHelper} from '@ensdomains/evm-verifier/contracts/EVMProofHelper.sol'; -import {EVMProofHelper2} from './EVMProofHelper2.sol'; import {IEVMVerifier} from '@ensdomains/evm-verifier/contracts/IEVMVerifier.sol'; +import {RLPReader} from "@eth-optimism/contracts-bedrock/src/libraries/rlp/RLPReader.sol"; + + +interface IScrollChain { + /// @param batchIndex The index of the batch. + /// @return The state root of a committed batch. + function finalizedStateRoots(uint256 batchIndex) external view returns (bytes32); +} interface IScrollChainCommitmentVerifier { function verifyZkTrieProof( @@ -11,6 +18,8 @@ interface IScrollChainCommitmentVerifier { bytes calldata proof ) external view returns (bytes32 stateRoot, bytes32 storageValue); + function rollup() external view returns (address); + function verifyStateCommitment( uint256 batchIndex, address account, @@ -58,6 +67,12 @@ contract ScrollVerifier is IEVMVerifier { return output; } + function getTrieProof(address target, uint256 slot, bytes[] memory compressedProof, bytes32 root) internal view returns(bytes memory){ + (bytes32 stateRoot, bytes32 storageValue) = verifier.verifyZkTrieProof(target, bytes32(slot), compressedProof[0]); + require(stateRoot == root, "stateRoot not matched"); + return abi.encodePacked(storageValue); + } + /* * Retrieves storage values from the specified target address * @@ -72,24 +87,17 @@ contract ScrollVerifier is IEVMVerifier { bytes[] memory constants, bytes memory proof ) external view returns (bytes[] memory values) { - values = new bytes[](commands.length); - for(uint256 i = 0; i < commands.length; i++) { - bytes32 command = commands[i]; - - (bool isDynamic, uint256 slot) = EVMProofHelper2.computeFirstSlot(command, constants, values); - (ScrollWitnessData memory scrollData, StateProof memory stateProof) = abi.decode(proof, (ScrollWitnessData, StateProof)); - bytes memory compressedProof = compressProof(stateProof.stateTrieWitness, stateProof.storageProofs, i); - (bytes32 stateRoot, bytes32 storageValue) = verifier.verifyZkTrieProof(target, scrollData.storageKeys[i], compressedProof); - if(!isDynamic) { - values[i] = abi.encodePacked(storageValue); - if(values[i].length > 32) { - revert InvalidSlotSize(values[i].length); - } - } else { - values[i] = abi.encodePacked(storageValue); - // TODO - // (values[i], proofIdx) = getDynamicValue(storageRoot, slot, proof, proofIdx); - } + (ScrollWitnessData memory scrollData, StateProof memory stateProof) = abi.decode(proof, (ScrollWitnessData, StateProof)); + bytes[][] memory compressedProofs = new bytes[][](stateProof.storageProofs.length); + for(uint256 i = 0; i < stateProof.storageProofs.length; i++) { + compressedProofs[i] = new bytes[](1); + compressedProofs[i][0] = compressProof(stateProof.stateTrieWitness, stateProof.storageProofs, i); } + + (bytes32 computedStateRoot, bytes32 storageValue) = verifier.verifyZkTrieProof(target, scrollData.storageKeys[0], compressedProofs[0][0]); + bytes32 expectedStateRoot = IScrollChain(verifier.rollup()).finalizedStateRoots(scrollData.batchIndex); + require(computedStateRoot == expectedStateRoot, "Invalid inclusion proof"); + + return EVMProofHelper.getStorageValues(target, getTrieProof, commands, constants, expectedStateRoot, compressedProofs); } } diff --git a/scroll-verifier/deploy_l1/00_arb_verifier.ts b/scroll-verifier/deploy_l1/00_scroll_verifier.ts similarity index 100% rename from scroll-verifier/deploy_l1/00_arb_verifier.ts rename to scroll-verifier/deploy_l1/00_scroll_verifier.ts