Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation of multi-target CCIP read #28

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 78 additions & 25 deletions evm-gateway/src/EVMGateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
solidityPackedKeccak256,
toBigInt,
zeroPadValue,
AbiCoder,
type AddressLike
} from 'ethers';

import type { IProofService, ProvableBlock } from './IProofService.js';
Expand All @@ -26,6 +28,7 @@ export enum StorageLayout {
}

interface StorageElement {
target: AddressLike,
slots: bigint[];
value: () => Promise<string>;
isDynamic: boolean;
Expand Down Expand Up @@ -67,6 +70,7 @@ export class EVMGateway<T extends ProvableBlock> {
*
* 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
Expand All @@ -91,16 +95,48 @@ export class EVMGateway<T extends ProvableBlock> {
* 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);
Expand All @@ -114,41 +150,53 @@ export class EVMGateway<T extends ProvableBlock> {

/**
*
* @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<string> {

const block = await this.proofService.getProvableBlock();
const requests: Promise<StorageElement>[] = [];
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<StorageElement>[]
allResults: string[]
): Promise<string> {
const opcode = operation & 0xe0;
const operand = operation & 0x1f;
Expand All @@ -157,7 +205,7 @@ export class EVMGateway<T extends ProvableBlock> {
case OP_CONSTANT:
return constants[operand];
case OP_BACKREF:
return await (await requests[operand]).value();
return allResults[operand];
default:
throw new Error('Unrecognized opcode');
}
Expand All @@ -166,22 +214,22 @@ export class EVMGateway<T extends ProvableBlock> {
private async computeFirstSlot(
command: string,
constants: string[],
requests: Promise<StorageElement>[]
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.
for (let j = 2; j < 32 && commandWord[j] != 0xff; j++) {
const index = await this.executeOperation(
commandWord[j],
constants,
requests
allResults
);
slot = toBigInt(
solidityPackedKeccak256(['bytes', 'uint256'], [index, slot])
Expand All @@ -193,7 +241,7 @@ export class EVMGateway<T extends ProvableBlock> {

private async getDynamicValue(
block: T,
address: string,
address: AddressLike,
slot: bigint
): Promise<StorageElement> {
const firstValue = getBytes(
Expand All @@ -208,6 +256,7 @@ export class EVMGateway<T extends ProvableBlock> {
.fill(BigInt(hashedSlot))
.map((i, idx) => i + BigInt(idx));
return {
target: address,
slots: Array.prototype.concat([slot], slotNumbers),
isDynamic: true,
value: memoize(async () => {
Expand All @@ -222,7 +271,9 @@ export class EVMGateway<T extends ProvableBlock> {
} 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)),
Expand All @@ -232,19 +283,21 @@ export class EVMGateway<T extends ProvableBlock> {

private async getValueFromPath(
block: T,
address: string,
address: AddressLike,
command: string,
constants: string[],
requests: Promise<StorageElement>[]
allResults: string[]
): Promise<StorageElement> {

const { slot, isDynamic } = await this.computeFirstSlot(
command,
constants,
requests
allResults
);

if (!isDynamic) {
return {
target: address,
slots: [slot],
isDynamic,
value: memoize(async () =>
Expand Down
64 changes: 57 additions & 7 deletions evm-verifier/contracts/EVMFetchTarget.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -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))
}
Expand Down
Loading