From b6dcc9f56512e0b94419a1321c687842a7195750 Mon Sep 17 00:00:00 2001 From: Thomas Clowes Date: Tue, 16 Jan 2024 14:23:11 +0000 Subject: [PATCH] Initial implementation of multi-target CCIP read --- evm-gateway/src/EVMGateway.ts | 103 ++++++--- evm-verifier/contracts/EVMFetchTarget.sol | 64 +++++- evm-verifier/contracts/EVMFetcher.sol | 247 ++++++++++++++++------ evm-verifier/contracts/IEVMGateway.sol | 14 ++ l1-gateway/package.json | 1 - l1-gateway/src/L1ProofService.ts | 2 +- l1-gateway/src/index.ts | 3 +- l1-gateway/src/server.ts | 2 +- l1-verifier/contracts/test/TestL1.sol | 62 +++++- l1-verifier/contracts/test/TestL2.sol | 16 +- l1-verifier/test/testL1Verifier.ts | 52 ++++- 11 files changed, 441 insertions(+), 125 deletions(-) create mode 100644 evm-verifier/contracts/IEVMGateway.sol diff --git a/evm-gateway/src/EVMGateway.ts b/evm-gateway/src/EVMGateway.ts index 7b3a2477..0f875700 100644 --- a/evm-gateway/src/EVMGateway.ts +++ b/evm-gateway/src/EVMGateway.ts @@ -7,6 +7,8 @@ import { solidityPackedKeccak256, toBigInt, zeroPadValue, + AbiCoder, + type AddressLike } from 'ethers'; import type { IProofService, ProvableBlock } from './IProofService.js'; @@ -26,6 +28,7 @@ export enum StorageLayout { } interface StorageElement { + target: AddressLike, slots: bigint[]; value: () => Promise; isDynamic: boolean; @@ -67,6 +70,7 @@ export class EVMGateway { * * Each command is a 32 byte value consisting of a single flags byte, followed by 31 instruction * bytes. Valid flags are: + * - 0x00 - Default. Static value. * - 0x01 - If set, the value to be returned is dynamic length. * * The VM implements a very simple stack machine, and instructions specify operations that happen on @@ -91,16 +95,48 @@ export class EVMGateway { * The final result of this hashing operation is used as the base slot number for the storage * lookup. This mirrors Solidity's recursive hashing operation for determining storage slot locations. */ - 'function getStorageSlots(address addr, bytes32[] memory commands, bytes[] memory constants) external view returns(bytes memory witness)', + 'function getStorageSlots(tuple(address target, bytes32[] commands, bytes[] constants, uint256 operationIdx)[] memory tRequests) external view returns( bytes[] memory proofs )', ]; + server.add(abi, [ { type: 'getStorageSlots', func: async (args) => { try { - const [addr, commands, constants] = args; - const proofs = await this.createProofs(addr, commands, constants); - return [proofs]; + + const coder = AbiCoder.defaultAbiCoder(); + + const [tRequests] = args; + + //Hold proofs for each target + const proofsArray: string[] = []; + + //Hold all requested values + const allResults: string[] = []; + + for (var request of tRequests) { + + var targetToUse = request.target; + + /** + * Replace referential targets + * Ethereum addresses are hexadecimal representations of uint160 values + * We will consider addresses using only the least significant byte (20) to be references to + * values pulled from previous targets. + * It is assumed that the values returned are valid addresses + */ + if (BigInt(targetToUse) <= 256) { + + targetToUse = coder.decode(["address"], allResults[0])[0]; + } + + const proofs = await this.createProofs(targetToUse, request.commands, request.constants, allResults); + + proofsArray.push(proofs); + } + + return [ proofsArray ]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { console.log(e.stack); @@ -114,41 +150,53 @@ export class EVMGateway { /** * - * @param address The address to fetch storage slot proofs for + * @param target The target address to fetch storage slot proofs for * @param paths Each element of this array specifies a Solidity-style path derivation for a storage slot ID. * See README.md for details of the encoding. */ async createProofs( - address: string, + target: AddressLike, commands: string[], - constants: string[] + constants: string[], + allResults: string[] ): Promise { + const block = await this.proofService.getProvableBlock(); - const requests: Promise[] = []; + const requests: StorageElement[] = []; // For each request, spawn a promise to compute the set of slots required + for (let i = 0; i < commands.length; i++) { + + const requestData = await this.getValueFromPath( + block, + target, + commands[i], + constants, + allResults + ); + requests.push( - this.getValueFromPath( - block, - address, - commands[i], - constants, - requests.slice() - ) + requestData ); + + const result = await requestData.value(); + allResults.push(result); } // Resolve all the outstanding requests const results = await Promise.all(requests); const slots = Array.prototype.concat( ...results.map((result) => result.slots) ); - return this.proofService.getProofs(block, address, slots); + + const proofs = await this.proofService.getProofs(block, target, slots); + + return proofs; } private async executeOperation( operation: number, constants: string[], - requests: Promise[] + allResults: string[] ): Promise { const opcode = operation & 0xe0; const operand = operation & 0x1f; @@ -157,7 +205,7 @@ export class EVMGateway { case OP_CONSTANT: return constants[operand]; case OP_BACKREF: - return await (await requests[operand]).value(); + return allResults[operand]; default: throw new Error('Unrecognized opcode'); } @@ -166,14 +214,14 @@ export class EVMGateway { private async computeFirstSlot( command: string, constants: string[], - requests: Promise[] + allResults: string[] ): Promise<{ slot: bigint; isDynamic: boolean }> { const commandWord = getBytes(command); const flags = commandWord[0]; const isDynamic = (flags & 0x01) != 0; let slot = toBigInt( - await this.executeOperation(commandWord[1], constants, requests) + await this.executeOperation(commandWord[1], constants, allResults) ); // If there are multiple path elements, recursively hash them solidity-style to get the final slot. @@ -181,7 +229,7 @@ export class EVMGateway { const index = await this.executeOperation( commandWord[j], constants, - requests + allResults ); slot = toBigInt( solidityPackedKeccak256(['bytes', 'uint256'], [index, slot]) @@ -193,7 +241,7 @@ export class EVMGateway { private async getDynamicValue( block: T, - address: string, + address: AddressLike, slot: bigint ): Promise { const firstValue = getBytes( @@ -208,6 +256,7 @@ export class EVMGateway { .fill(BigInt(hashedSlot)) .map((i, idx) => i + BigInt(idx)); return { + target: address, slots: Array.prototype.concat([slot], slotNumbers), isDynamic: true, value: memoize(async () => { @@ -222,7 +271,9 @@ export class EVMGateway { } else { // Short value: least significant byte is `length * 2`, other bytes are data. const len = firstValue[31] / 2; + return { + target: address, slots: [slot], isDynamic: true, value: () => Promise.resolve(dataSlice(firstValue, 0, len)), @@ -232,19 +283,21 @@ export class EVMGateway { private async getValueFromPath( block: T, - address: string, + address: AddressLike, command: string, constants: string[], - requests: Promise[] + allResults: string[] ): Promise { + const { slot, isDynamic } = await this.computeFirstSlot( command, constants, - requests + allResults ); if (!isDynamic) { return { + target: address, slots: [slot], isDynamic, value: memoize(async () => diff --git a/evm-verifier/contracts/EVMFetchTarget.sol b/evm-verifier/contracts/EVMFetchTarget.sol index 6ddc945f..7af84729 100644 --- a/evm-verifier/contracts/EVMFetchTarget.sol +++ b/evm-verifier/contracts/EVMFetchTarget.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.17; import { IEVMVerifier } from './IEVMVerifier.sol'; +import { IEVMGateway } from './IEVMGateway.sol'; import { Address } from '@openzeppelin/contracts/utils/Address.sol'; /** @@ -11,20 +12,69 @@ import { Address } from '@openzeppelin/contracts/utils/Address.sol'; abstract contract EVMFetchTarget { using Address for address; + error TargetProofMismatch(uint256 actual, uint256 expected); error ResponseLengthMismatch(uint256 actual, uint256 expected); + error TooManyReturnValues(uint256 max); + + uint256 constant MAX_RETURN_VALUES = 32; /** * @dev Internal callback function invoked by CCIP-Read in response to a `getStorageSlots` request. */ function getStorageSlotsCallback(bytes calldata response, bytes calldata extradata) external { - bytes memory proof = abi.decode(response, (bytes)); - (IEVMVerifier verifier, address addr, bytes32[] memory commands, bytes[] memory constants, bytes4 callback, bytes memory callbackData) = - abi.decode(extradata, (IEVMVerifier, address, bytes32[], bytes[], bytes4, bytes)); - bytes[] memory values = verifier.getStorageValues(addr, commands, constants, proof); - if(values.length != commands.length) { - revert ResponseLengthMismatch(values.length, commands.length); + + //Decode proofs from the response + (bytes[] memory proofs) = abi.decode(response, (bytes[])); + + //Decode the extradata + (IEVMVerifier verifier, IEVMGateway.EVMTargetRequest[] memory tRequests, bytes4 callback, bytes memory callbackData) = + abi.decode(extradata, (IEVMVerifier, IEVMGateway.EVMTargetRequest[], bytes4, bytes)); + + //We proove all returned data on a per target basis + if(tRequests.length != proofs.length) { + revert TargetProofMismatch(tRequests.length, proofs.length); } - bytes memory ret = address(this).functionCall(abi.encodeWithSelector(callback, values, callbackData)); + + bytes[] memory returnValues = new bytes[](MAX_RETURN_VALUES); + + uint k = 0; + + for (uint i = 0; i < tRequests.length; i++) { + + IEVMGateway.EVMTargetRequest memory tRequest = tRequests[i]; + + address targetToUse = tRequest.target; + + { + uint160 targetAsInt = uint160(bytes20(tRequest.target)); + + if (targetAsInt <= 256) { + + targetToUse = abi.decode(returnValues[0], (address)); + } + } + + bytes[] memory values = verifier.getStorageValues(targetToUse, tRequest.commands, tRequest.constants, proofs[i]); + + if(values.length != tRequest.commands.length) { + revert ResponseLengthMismatch(values.length, tRequest.commands.length); + } + + for (uint j = 0; j < values.length; j++) { + returnValues[k] = values[j]; + k++; + } + } + + assembly { + mstore(returnValues, k) // Increment returnValues array length + } + if(k > MAX_RETURN_VALUES) { + revert TooManyReturnValues(MAX_RETURN_VALUES); + } + + bytes memory ret = address(this).functionCall(abi.encodeWithSelector(callback, returnValues, callbackData)); + assembly { return(add(ret, 32), mload(ret)) } diff --git a/evm-verifier/contracts/EVMFetcher.sol b/evm-verifier/contracts/EVMFetcher.sol index 5b56b15e..e706c217 100644 --- a/evm-verifier/contracts/EVMFetcher.sol +++ b/evm-verifier/contracts/EVMFetcher.sol @@ -3,12 +3,10 @@ pragma solidity ^0.8.17; import { IEVMVerifier } from './IEVMVerifier.sol'; import { EVMFetchTarget } from './EVMFetchTarget.sol'; +import { IEVMGateway } from './IEVMGateway.sol'; import { Address } from '@openzeppelin/contracts/utils/Address.sol'; -interface IEVMGateway { - function getStorageSlots(address addr, bytes32[] memory commands, bytes[] memory constants) external pure returns(bytes memory witness); -} - +uint8 constant FLAG_STATIC = 0x00; uint8 constant FLAG_DYNAMIC = 0x01; uint8 constant OP_CONSTANT = 0x00; uint8 constant OP_BACKREF = 0x20; @@ -19,70 +17,121 @@ uint8 constant OP_END = 0xff; * See l1-verifier/test/TestL1.sol for example usage. */ library EVMFetcher { + uint256 constant MAX_TARGETS = 32; uint256 constant MAX_COMMANDS = 32; uint256 constant MAX_CONSTANTS = 32; // Must not be greater than 32 using Address for address; error TooManyCommands(uint256 max); + error TooManyTargets(uint256 max); error CommandTooLong(); error InvalidReference(uint256 value, uint256 max); error OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData); struct EVMFetchRequest { IEVMVerifier verifier; - address target; - bytes32[] commands; - uint256 operationIdx; - bytes[] constants; + IEVMGateway.EVMTargetRequest[] tRequests; } /** - * @dev Creates a request to fetch the value of multiple storage slots from a contract via CCIP-Read, possibly from - * another chain. - * Supports dynamic length values and slot numbers derived from other retrieved values. - * @param verifier An instance of a verifier contract that can provide and verify the storage slot information. - * @param target The address of the contract to fetch storage proofs for. + * @dev Creates a new EVMTargetRequest + * Internal helper function for DRY code + * @param request The request object being operated on. + * @param target The new target address */ - function newFetchRequest(IEVMVerifier verifier, address target) internal pure returns (EVMFetchRequest memory) { + function addNewTargetRequest(EVMFetchRequest memory request, address target) internal pure { + + IEVMGateway.EVMTargetRequest[] memory targetRequests = request.tRequests; + uint256 targetCount = targetRequests.length; + bytes32[] memory commands = new bytes32[](MAX_COMMANDS); bytes[] memory constants = new bytes[](MAX_CONSTANTS); assembly { + mstore(targetRequests, add(targetCount, 1)) //Set the array length to 1 (for the initial target) mstore(commands, 0) // Set current array length to 0 mstore(constants, 0) } - return EVMFetchRequest(verifier, target, commands, 0, constants); + + if(targetRequests.length > MAX_TARGETS) { + revert TooManyTargets(MAX_TARGETS); + } + + targetRequests[targetCount] = IEVMGateway.EVMTargetRequest(target, commands, constants, 0); } /** - * @dev Starts describing a new fetch request. - * Paths specify a series of hashing operations to derive the final slot ID. - * See https://docs.soliditylang.org/en/v0.8.17/internals/layout_in_storage.html for details on how Solidity - * lays out storage variables. + * @dev Creates a request to fetch the value of multiple storage slots from one or more target contracts + * via CCIP-Read, possibly from another chain. + * Supports dynamic length values and slot numbers derived from other retrieved values. + * @param verifier An instance of a verifier contract that can provide and verify the storage slot information. + * @param target The address of the contract to fetch storage proofs for. + */ + function newFetchRequest(IEVMVerifier verifier, address target) internal pure returns (EVMFetchRequest memory) { + + //Reserve the space + IEVMGateway.EVMTargetRequest[] memory targetRequests = new IEVMGateway.EVMTargetRequest[](MAX_TARGETS); + + assembly { + mstore(targetRequests, 0) + } + + //Create the base EVMFetchRequest + EVMFetchRequest memory request = EVMFetchRequest(verifier, targetRequests); + + //Add a new IEVMGateway.EVMTargetRequest + addNewTargetRequest(request, target); + + return request; + } + + /** + * @dev Initializes a new EVM command for fetching a data value from a target + * Internal helper function for DRY code * @param request The request object being operated on. * @param baseSlot The base slot ID that forms the root of the path. + * @param flag The command flag specifying if the requested value is housed in a static or dynamic storage slot */ - function getStatic(EVMFetchRequest memory request, uint256 baseSlot) internal pure returns (EVMFetchRequest memory) { - bytes32[] memory commands = request.commands; + function initNewCommand(EVMFetchRequest memory request, uint256 baseSlot, uint8 flag) internal pure { + + uint256 targetIdx = request.tRequests.length - 1; + IEVMGateway.EVMTargetRequest memory tRequest = request.tRequests[targetIdx]; + bytes32[] memory commands = tRequest.commands; + uint256 commandIdx = commands.length; - if(commandIdx > 0 && request.operationIdx < 32) { + if(commandIdx > 0 && tRequest.operationIdx < 32) { // Terminate previous command - _addOperation(request, OP_END); + _addOperation(tRequest, OP_END); } assembly { mstore(commands, add(commandIdx, 1)) // Increment command array length } - if(request.commands.length > MAX_COMMANDS) { + if(commands.length > MAX_COMMANDS) { revert TooManyCommands(MAX_COMMANDS); } - request.operationIdx = 0; - _addOperation(request, 0); - _addOperation(request, _addConstant(request, abi.encode(baseSlot))); + + tRequest.operationIdx = 0; + _addOperation(tRequest, flag); + _addOperation(tRequest, _addConstant(tRequest, abi.encode(baseSlot))); + } + + /** + * @dev Initialize a command to fetch a data value from a single static storage slot + * See https://docs.soliditylang.org/en/v0.8.17/internals/layout_in_storage.html for details on how Solidity + * lays out storage variables. + * @param request The request object being operated on. + * @param baseSlot The base slot ID that forms the root of the path. + */ + function getStatic(EVMFetchRequest memory request, uint256 baseSlot) internal pure returns (EVMFetchRequest memory) { + + initNewCommand(request, baseSlot, FLAG_STATIC); + return request; } /** - * @dev Starts describing a new fetch request. + * @dev Initialize a command to fetch a dynamic data value from multiple storage slots + * subject to the Solidity storage rules * Paths specify a series of hashing operations to derive the final slot ID. * See https://docs.soliditylang.org/en/v0.8.17/internals/layout_in_storage.html for details on how Solidity * lays out storage variables. @@ -90,21 +139,9 @@ library EVMFetcher { * @param baseSlot The base slot ID that forms the root of the path. */ function getDynamic(EVMFetchRequest memory request, uint256 baseSlot) internal pure returns (EVMFetchRequest memory) { - bytes32[] memory commands = request.commands; - uint256 commandIdx = commands.length; - if(commandIdx > 0 && request.operationIdx < 32) { - // Terminate previous command - _addOperation(request, OP_END); - } - assembly { - mstore(commands, add(commandIdx, 1)) // Increment command array length - } - if(request.commands.length > MAX_COMMANDS) { - revert TooManyCommands(MAX_COMMANDS); - } - request.operationIdx = 0; - _addOperation(request, FLAG_DYNAMIC); - _addOperation(request, _addConstant(request, abi.encode(baseSlot))); + + initNewCommand(request, baseSlot, FLAG_DYNAMIC); + return request; } @@ -114,10 +151,15 @@ library EVMFetcher { * @param el The element to add. */ function element(EVMFetchRequest memory request, uint256 el) internal pure returns (EVMFetchRequest memory) { - if(request.operationIdx >= 32) { + + uint256 targetIdx = request.tRequests.length - 1; + IEVMGateway.EVMTargetRequest memory tRequest = request.tRequests[targetIdx]; + + if(tRequest.operationIdx >= 32) { revert CommandTooLong(); } - _addOperation(request, _addConstant(request, abi.encode(el))); + _addOperation(tRequest, _addConstant(tRequest, abi.encode(el))); + return request; } @@ -127,10 +169,15 @@ library EVMFetcher { * @param el The element to add. */ function element(EVMFetchRequest memory request, bytes32 el) internal pure returns (EVMFetchRequest memory) { - if(request.operationIdx >= 32) { + + uint256 targetIdx = request.tRequests.length - 1; + IEVMGateway.EVMTargetRequest memory tRequest = request.tRequests[targetIdx]; + + if(tRequest.operationIdx >= 32) { revert CommandTooLong(); } - _addOperation(request, _addConstant(request, abi.encode(el))); + _addOperation(tRequest, _addConstant(tRequest, abi.encode(el))); + return request; } @@ -140,10 +187,14 @@ library EVMFetcher { * @param el The element to add. */ function element(EVMFetchRequest memory request, address el) internal pure returns (EVMFetchRequest memory) { - if(request.operationIdx >= 32) { + + uint256 targetIdx = request.tRequests.length - 1; + IEVMGateway.EVMTargetRequest memory tRequest = request.tRequests[targetIdx]; + + if(tRequest.operationIdx >= 32) { revert CommandTooLong(); } - _addOperation(request, _addConstant(request, abi.encode(el))); + _addOperation(tRequest, _addConstant(tRequest, abi.encode(el))); return request; } @@ -153,10 +204,14 @@ library EVMFetcher { * @param el The element to add. */ function element(EVMFetchRequest memory request, bytes memory el) internal pure returns (EVMFetchRequest memory) { - if(request.operationIdx >= 32) { + + uint256 targetIdx = request.tRequests.length - 1; + IEVMGateway.EVMTargetRequest memory tRequest = request.tRequests[targetIdx]; + + if(tRequest.operationIdx >= 32) { revert CommandTooLong(); } - _addOperation(request, _addConstant(request, el)); + _addOperation(tRequest, _addConstant(tRequest, el)); return request; } @@ -166,10 +221,14 @@ library EVMFetcher { * @param el The element to add. */ function element(EVMFetchRequest memory request, string memory el) internal pure returns (EVMFetchRequest memory) { - if(request.operationIdx >= 32) { + + uint256 targetIdx = request.tRequests.length - 1; + IEVMGateway.EVMTargetRequest memory tRequest = request.tRequests[targetIdx]; + + if(tRequest.operationIdx >= 32) { revert CommandTooLong(); } - _addOperation(request, _addConstant(request, bytes(el))); + _addOperation(tRequest, _addConstant(tRequest, bytes(el))); return request; } @@ -179,13 +238,58 @@ library EVMFetcher { * @param idx The index of the previous fetch request, starting at 0. */ function ref(EVMFetchRequest memory request, uint8 idx) internal pure returns (EVMFetchRequest memory) { - if(request.operationIdx >= 32) { + + uint256 targetIdx = request.tRequests.length - 1; + IEVMGateway.EVMTargetRequest memory tRequest = request.tRequests[targetIdx]; + + if(tRequest.operationIdx >= 32) { revert CommandTooLong(); } - if(idx > request.commands.length || idx > 31) { - revert InvalidReference(idx, request.commands.length); + if(idx > tRequest.commands.length || idx > 31) { + revert InvalidReference(idx, tRequest.commands.length); } - _addOperation(request, OP_BACKREF | idx); + _addOperation(tRequest, OP_BACKREF | idx); + return request; + } + + /** + * @dev Sets the target contract address from which to fetch future requested values + * @param request The request object being operated on. + * @param target The target contract address + */ + function setTarget(EVMFetchRequest memory request, address target) internal pure returns (EVMFetchRequest memory) { + + IEVMGateway.EVMTargetRequest[] memory tRequests = request.tRequests; + + uint256 targetCount = tRequests.length; + uint256 targetIdx = targetCount - 1; + + //Close off the last command for the previous IEVMGateway.EVMTargetRequest + IEVMGateway.EVMTargetRequest memory tRequest = request.tRequests[targetIdx]; + bytes32[] memory commands = tRequest.commands; + + if(commands.length > 0 && tRequest.operationIdx < 32) { + // Terminate last command + _addOperation(tRequest, OP_END); + } + + //Add a new IEVMGateway.EVMTargetRequest + addNewTargetRequest(request, target); + + return request; + } + + /** + * @dev Sets the target contract address from which to fetch future requested values + * to an address returned as a value from a previous indexed request + * @param request The request object being operated on. + * @param idx The index of the value requested that contains an address to target + */ + function setTargetRef(EVMFetchRequest memory request, uint8 idx) internal pure returns (EVMFetchRequest memory) { + + address targetAddress = address(uint160(bytes20(bytes1(idx)) >> (152))); + setTarget(request, targetAddress); + return request; } @@ -199,30 +303,41 @@ library EVMFetcher { * @param callbackData Extra data to supply to the callback. */ function fetch(EVMFetchRequest memory request, bytes4 callbackId, bytes memory callbackData) internal view { - if(request.commands.length > 0 && request.operationIdx < 32) { + + uint256 targetIdx = request.tRequests.length - 1; + IEVMGateway.EVMTargetRequest memory tRequest = request.tRequests[targetIdx]; + + bytes[] memory constants = tRequest.constants; + + if(tRequest.commands.length > 0 && tRequest.operationIdx < 32) { // Terminate last command - _addOperation(request, OP_END); + _addOperation(tRequest, OP_END); } + revert OffchainLookup( address(this), request.verifier.gatewayURLs(), - abi.encodeCall(IEVMGateway.getStorageSlots, (request.target, request.commands, request.constants)), + abi.encodeCall(IEVMGateway.getStorageSlots, request.tRequests), EVMFetchTarget.getStorageSlotsCallback.selector, - abi.encode(request.verifier, request.target, request.commands, request.constants, callbackId, callbackData) + abi.encode(request.verifier, request.tRequests, callbackId, callbackData) ); } - function _addConstant(EVMFetchRequest memory request, bytes memory value) private pure returns(uint8 idx) { - bytes[] memory constants = request.constants; + function _addConstant(IEVMGateway.EVMTargetRequest memory tRequest, bytes memory value) private pure returns(uint8 idx) { + + bytes[] memory constants = tRequest.constants; + idx = uint8(constants.length); + assembly { mstore(constants, add(idx, 1)) // Increment constant array length } constants[idx] = value; } - function _addOperation(EVMFetchRequest memory request, uint8 op) private pure { - uint256 commandIdx = request.commands.length - 1; - request.commands[commandIdx] = request.commands[commandIdx] | (bytes32(bytes1(op)) >> (8 * request.operationIdx++)); + function _addOperation(IEVMGateway.EVMTargetRequest memory tRequest, uint8 op) private pure { + + uint256 commandIdx = tRequest.commands.length - 1; + tRequest.commands[commandIdx] = tRequest.commands[commandIdx] | (bytes32(bytes1(op)) >> (8 * tRequest.operationIdx++)); } } diff --git a/evm-verifier/contracts/IEVMGateway.sol b/evm-verifier/contracts/IEVMGateway.sol new file mode 100644 index 00000000..c3e10a64 --- /dev/null +++ b/evm-verifier/contracts/IEVMGateway.sol @@ -0,0 +1,14 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +interface IEVMGateway { + + struct EVMTargetRequest { + address target; + bytes32[] commands; + bytes[] constants; + uint256 operationIdx; + } + + function getStorageSlots(EVMTargetRequest[] memory tRequests) external pure returns(bytes memory witness); +} \ No newline at end of file diff --git a/l1-gateway/package.json b/l1-gateway/package.json index e63432b3..d6d2fbaa 100644 --- a/l1-gateway/package.json +++ b/l1-gateway/package.json @@ -58,7 +58,6 @@ "dependencies": { "@chainlink/ccip-read-server": "^0.2.1", "@commander-js/extra-typings": "^11.0.0", - "@ensdomains/evm-gateway": "^0.1.0", "@ethereumjs/block": "^5.0.0", "@nomicfoundation/ethereumjs-block": "^5.0.2", "commander": "^11.0.0", diff --git a/l1-gateway/src/L1ProofService.ts b/l1-gateway/src/L1ProofService.ts index 15d5880e..e44ba457 100644 --- a/l1-gateway/src/L1ProofService.ts +++ b/l1-gateway/src/L1ProofService.ts @@ -5,7 +5,7 @@ import { type JsonRpcProvider, } from 'ethers'; -import { EVMProofHelper, type IProofService } from '@ensdomains/evm-gateway'; +import { EVMProofHelper, type IProofService } from '../../evm-gateway'; import { Block, type JsonRpcBlock } from '@ethereumjs/block'; type RlpObject = Uint8Array | Array; diff --git a/l1-gateway/src/index.ts b/l1-gateway/src/index.ts index 68e1c08c..83acd146 100644 --- a/l1-gateway/src/index.ts +++ b/l1-gateway/src/index.ts @@ -1,4 +1,5 @@ -import { EVMGateway } from '@ensdomains/evm-gateway'; +import { EVMGateway } from '../../evm-gateway'; + import { JsonRpcProvider } from 'ethers'; import { L1ProofService, type L1ProvableBlock } from './L1ProofService.js'; diff --git a/l1-gateway/src/server.ts b/l1-gateway/src/server.ts index 058bec29..465ffd69 100644 --- a/l1-gateway/src/server.ts +++ b/l1-gateway/src/server.ts @@ -1,6 +1,6 @@ import { Server } from '@chainlink/ccip-read-server'; import { Command } from '@commander-js/extra-typings'; -import { EVMGateway } from '@ensdomains/evm-gateway'; +import { EVMGateway } from '../../evm-gateway'; import { ethers } from 'ethers'; import { L1ProofService } from './L1ProofService.js'; diff --git a/l1-verifier/contracts/test/TestL1.sol b/l1-verifier/contracts/test/TestL1.sol index 347d8bc6..edaee315 100644 --- a/l1-verifier/contracts/test/TestL1.sol +++ b/l1-verifier/contracts/test/TestL1.sol @@ -10,10 +10,12 @@ contract TestL1 is EVMFetchTarget { IEVMVerifier verifier; // Slot 0 address target; + address target2; - constructor(IEVMVerifier _verifier, address _target) { + constructor(IEVMVerifier _verifier, address _target, address _target2) { verifier = _verifier; target = _target; + target2 = _target2; } function getLatest() public view returns(uint256) { @@ -26,9 +28,47 @@ contract TestL1 is EVMFetchTarget { return abi.decode(values[0], (uint256)); } + function getSecondAddress() public view returns(address) { + EVMFetcher.newFetchRequest(verifier, target) + .getStatic(1) + .fetch(this.getSecondAddressCallback.selector, ""); + } + + function getSecondAddressCallback(bytes[] memory values, bytes memory) public pure returns(address) { + return abi.decode(values[0], (address)); + } + + function getValueFromSecondContract() public view returns(address) { + EVMFetcher.newFetchRequest(verifier, target) + .getStatic(1) + .setTargetRef(0) + .getStatic(1) + .fetch(this.getValueFromSecondContractCallback.selector, ""); + } + + function getValueFromSecondContractCallback(bytes[] memory values, bytes memory) public pure returns(address) { + return abi.decode(values[1], (address)); + } + + function getLatestFromTwoTargets() public view returns(uint256) { + + EVMFetcher.newFetchRequest(verifier, target) + .getStatic(0) + .setTarget(target2) + .getStatic(0) + .fetch(this.getLatestFromTwoTargetsCallback.selector, ""); + } + + function getLatestFromTwoTargetsCallback(bytes[] memory values, bytes memory) public pure returns(uint256) { + uint256 l1 = abi.decode(values[0], (uint256)); + uint256 l2 = abi.decode(values[1], (uint256)); + + return l1 + l2; + } + function getName() public view returns(string memory) { EVMFetcher.newFetchRequest(verifier, target) - .getDynamic(1) + .getDynamic(2) .fetch(this.getNameCallback.selector, ""); } @@ -38,7 +78,7 @@ contract TestL1 is EVMFetchTarget { function getHighscorer(uint256 idx) public view returns(string memory) { EVMFetcher.newFetchRequest(verifier, target) - .getDynamic(3) + .getDynamic(4) .element(idx) .fetch(this.getHighscorerCallback.selector, ""); } @@ -50,7 +90,7 @@ contract TestL1 is EVMFetchTarget { function getLatestHighscore() public view returns(uint256) { EVMFetcher.newFetchRequest(verifier, target) .getStatic(0) - .getStatic(2) + .getStatic(3) .ref(0) .fetch(this.getLatestHighscoreCallback.selector, ""); } @@ -62,7 +102,7 @@ contract TestL1 is EVMFetchTarget { function getLatestHighscorer() public view returns(string memory) { EVMFetcher.newFetchRequest(verifier, target) .getStatic(0) - .getDynamic(3) + .getDynamic(4) .ref(0) .fetch(this.getLatestHighscorerCallback.selector, ""); } @@ -73,7 +113,7 @@ contract TestL1 is EVMFetchTarget { function getNickname(string memory _name) public view returns(string memory) { EVMFetcher.newFetchRequest(verifier, target) - .getDynamic(4) + .getDynamic(5) .element(_name) .fetch(this.getNicknameCallback.selector, ""); } @@ -84,8 +124,8 @@ contract TestL1 is EVMFetchTarget { function getPrimaryNickname() public view returns(string memory) { EVMFetcher.newFetchRequest(verifier, target) - .getDynamic(1) - .getDynamic(4) + .getDynamic(2) + .getDynamic(5) .ref(0) .fetch(this.getPrimaryNicknameCallback.selector, ""); } @@ -96,7 +136,7 @@ contract TestL1 is EVMFetchTarget { function getZero() public view returns(uint256) { EVMFetcher.newFetchRequest(verifier, target) - .getStatic(5) + .getStatic(6) .fetch(this.getZeroCallback.selector, ""); } @@ -106,8 +146,8 @@ contract TestL1 is EVMFetchTarget { function getZeroIndex() public view returns(uint256) { EVMFetcher.newFetchRequest(verifier, target) - .getStatic(5) - .getStatic(2) + .getStatic(6) + .getStatic(3) .ref(0) .fetch(this.getZeroIndexCallback.selector, ""); } diff --git a/l1-verifier/contracts/test/TestL2.sol b/l1-verifier/contracts/test/TestL2.sol index fc8fc645..2c7d9da8 100644 --- a/l1-verifier/contracts/test/TestL2.sol +++ b/l1-verifier/contracts/test/TestL2.sol @@ -3,14 +3,16 @@ pragma solidity ^0.8.17; contract TestL2 { uint256 latest; // Slot 0 - string name; // Slot 1 - mapping(uint256=>uint256) highscores; // Slot 2 - mapping(uint256=>string) highscorers; // Slot 3 - mapping(string=>string) realnames; // Slot 4 - uint256 zero; // Slot 5 + address secondAddress; // Slot 1 + string name; // Slot 2 + mapping(uint256=>uint256) highscores; // Slot 3 + mapping(uint256=>string) highscorers; // Slot 4 + mapping(string=>string) realnames; // Slot 5 + uint256 zero; // Slot 6 - constructor() { - latest = 42; + constructor(uint256 _latestNumber, address _secondAddress) { + latest = _latestNumber; + secondAddress = _secondAddress; name = "Satoshi"; highscores[0] = 1; highscores[latest] = 12345; diff --git a/l1-verifier/test/testL1Verifier.ts b/l1-verifier/test/testL1Verifier.ts index 7ea4bbf8..09929891 100644 --- a/l1-verifier/test/testL1Verifier.ts +++ b/l1-verifier/test/testL1Verifier.ts @@ -1,9 +1,10 @@ import { Server } from '@chainlink/ccip-read-server'; -import { makeL1Gateway } from '@ensdomains/l1-gateway'; +import { makeL1Gateway } from '../../l1-gateway'; import { HardhatEthersProvider } from '@nomicfoundation/hardhat-ethers/internal/hardhat-ethers-provider'; import type { HardhatEthersHelpers } from '@nomicfoundation/hardhat-ethers/types'; import { expect } from 'chai'; import { + AddressLike, BrowserProvider, Contract, FetchRequest, @@ -29,6 +30,10 @@ declare module 'hardhat/types/runtime' { } } +const SECOND_TARGET_RESPONSE_ADDRESS = "0x0000000000000000000000000000000000000123"; + +var l2contractAddress: AddressLike; + describe('L1Verifier', () => { let provider: BrowserProvider; let signer: Signer; @@ -56,6 +61,7 @@ describe('L1Verifier', () => { ); } const response = await r; + return { statusCode: response.statusCode, statusMessage: response.ok ? 'OK' : response.statusCode.toString(), @@ -72,32 +78,59 @@ describe('L1Verifier', () => { verifier = await l1VerifierFactory.deploy(['test:']); const testL2Factory = await ethers.getContractFactory('TestL2', signer); - const l2contract = await testL2Factory.deploy(); + const l2contract = await testL2Factory.deploy(42, SECOND_TARGET_RESPONSE_ADDRESS); + + await l2contract.waitForDeployment(); + l2contractAddress = await l2contract.getAddress(); + + const l2contractB = await testL2Factory.deploy(262, l2contractAddress); + await l2contractB.waitForDeployment(); + const l2contractBAddress = await l2contractB.getAddress(); const testL1Factory = await ethers.getContractFactory('TestL1', signer); target = await testL1Factory.deploy( await verifier.getAddress(), - await l2contract.getAddress() + await l2contractBAddress, + await l2contractAddress, ); + // Mine an empty block so we have something to prove against await provider.send('evm_mine', []); }); + it('simple proofs for fixed values', async () => { const result = await target.getLatest({ enableCcipRead: true }); - expect(Number(result)).to.equal(42); + + expect(Number(result)).to.equal(262); + }); + + + it('simple proofs for fixed values from two targets', async () => { + const result = await target.getLatestFromTwoTargets({ enableCcipRead: true }); + + expect(Number(result)).to.equal(304); }); + it('simple proofs for dynamic values', async () => { const result = await target.getName({ enableCcipRead: true }); expect(result).to.equal('Satoshi'); }); + + it('simple proofs for address', async () => { + const result = await target.getSecondAddress({ enableCcipRead: true }); + expect(result).to.equal(l2contractAddress); + }); + + it('nested proofs for dynamic values', async () => { - const result = await target.getHighscorer(42, { enableCcipRead: true }); + const result = await target.getHighscorer(262, { enableCcipRead: true }); expect(result).to.equal('Hal Finney'); }); + it('nested proofs for long dynamic values', async () => { const result = await target.getHighscorer(1, { enableCcipRead: true }); expect(result).to.equal( @@ -105,16 +138,25 @@ describe('L1Verifier', () => { ); }); + it('nested proofs with lookbehind', async () => { const result = await target.getLatestHighscore({ enableCcipRead: true }); expect(Number(result)).to.equal(12345); }); + + + it('simple proofs for address target', async () => { + const result = await target.getValueFromSecondContract({ enableCcipRead: true }); + expect(result).to.equal(SECOND_TARGET_RESPONSE_ADDRESS); + }); + it('nested proofs with lookbehind for dynamic values', async () => { const result = await target.getLatestHighscorer({ enableCcipRead: true }); expect(result).to.equal('Hal Finney'); }); + it('mappings with variable-length keys', async () => { const result = await target.getNickname('Money Skeleton', { enableCcipRead: true,