diff --git a/arb-gateway/src/ArbProofService.ts b/arb-gateway/src/ArbProofService.ts index fc4cac9b..f31aad03 100644 --- a/arb-gateway/src/ArbProofService.ts +++ b/arb-gateway/src/ArbProofService.ts @@ -1,5 +1,5 @@ /* eslint-disable prettier/prettier */ -import { EVMProofHelper, type IProofService } from '@ensdomains/evm-gateway'; +import { EVMProofHelper, convertIntoMerkleTrieProof, type IProofService } from '@ensdomains/evm-gateway'; import { AbiCoder, Contract, EventLog, ethers, toBeHex, type AddressLike, toNumber } from 'ethers'; import rollupAbi from "./abi/rollupABI.js"; import type { IBlockCache } from './blockCache/IBlockCache.js'; @@ -63,7 +63,7 @@ export class ArbProofService implements IProofService { return AbiCoder.defaultAbiCoder().encode( [ 'tuple(bytes32 version, bytes32 sendRoot, uint64 nodeIndex,bytes rlpEncodedBlock)', - 'tuple(bytes[] stateTrieWitness, bytes[][] storageProofs)', + 'tuple(bytes stateTrieWitness, bytes[] storageProofs)', ], [ { @@ -73,7 +73,7 @@ export class ArbProofService implements IProofService { nodeIndex: block.nodeIndex, rlpEncodedBlock: block.rlpEncodedBlock }, - proof, + convertIntoMerkleTrieProof(proof) ] ); } 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..75f972d4 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 {MerkleTrieProofHelper} from '@ensdomains/evm-verifier/contracts/MerkleTrieProofHelper.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 = MerkleTrieProofHelper.getStorageRoot(stateRoot, target, stateProof.stateTrieWitness); + return EVMProofHelper.getStorageValues(target, MerkleTrieProofHelper.getTrieProof, commands, constants, storageRoot, stateProof.storageProofs); } /* diff --git a/arb-verifier/test/testArbVerifier.ts b/arb-verifier/test/testArbVerifier.ts index 99edc8f8..aa304fc3 100644 --- a/arb-verifier/test/testArbVerifier.ts +++ b/arb-verifier/test/testArbVerifier.ts @@ -4,6 +4,8 @@ import { HardhatEthersProvider } from '@nomicfoundation/hardhat-ethers/internal/ import type { HardhatEthersHelpers } from '@nomicfoundation/hardhat-ethers/types'; import { expect } from 'chai'; import { + AbiCoder, + concat, Contract, FetchRequest, Provider, @@ -28,6 +30,23 @@ declare module 'hardhat/types/runtime' { ethers: ethersObj; } } +const estimateCCIPReadCallbackGas = async (provider, cb) => { + try{ + await cb() + }catch(e){ + const [sender, urls, data, callbackFunction, extraData ] = e.revert.args + const url = `http://localhost:8080/${sender}/${data}.json` + const responseData:any = await (await fetch(url)).json() + const encoder = new AbiCoder() + const encoded = encoder.encode([ "bytes", "bytes" ], [responseData.data, extraData]); + const newdata = concat([ callbackFunction, encoded ]) + const result2 = await provider.estimateGas({ + to: sender, + data:newdata + }); + console.log(`Gas estimate ${result2}`) + } +} describe('ArbVerifier', () => { let provider: Provider; @@ -87,16 +106,25 @@ describe('ArbVerifier', () => { it('simple proofs for fixed values', async () => { const result = await target.getLatest({ enableCcipRead: true }); expect(Number(result)).to.equal(42); + await estimateCCIPReadCallbackGas(provider, ()=>{ + return target.getLatest({ enableCcipRead: false }); + }) }); it('simple proofs for dynamic values', async () => { const result = await target.getName({ enableCcipRead: true }); expect(result).to.equal('Satoshi'); + await estimateCCIPReadCallbackGas(provider, ()=>{ + return target.getName({ enableCcipRead: false }); + }) }); it('nested proofs for dynamic values', async () => { const result = await target.getHighscorer(42, { enableCcipRead: true }); expect(result).to.equal('Hal Finney'); + await estimateCCIPReadCallbackGas(provider, ()=>{ + return target.getHighscorer(42, { enableCcipRead: false }); + }) }); it('nested proofs for long dynamic values', async () => { @@ -104,16 +132,25 @@ describe('ArbVerifier', () => { expect(result).to.equal( 'Hubert Blaine Wolfeschlegelsteinhausenbergerdorff Sr.' ); + await estimateCCIPReadCallbackGas(provider, ()=>{ + return target.getHighscorer(1, { enableCcipRead: false }); + }) }); it('nested proofs with lookbehind', async () => { const result = await target.getLatestHighscore({ enableCcipRead: true }); expect(Number(result)).to.equal(12345); + await estimateCCIPReadCallbackGas(provider, ()=>{ + return target.getLatestHighscore({ enableCcipRead: false }); + }) }); it('nested proofs with lookbehind for dynamic values', async () => { const result = await target.getLatestHighscorer({ enableCcipRead: true }); expect(result).to.equal('Hal Finney'); + await estimateCCIPReadCallbackGas(provider, ()=>{ + return target.getLatestHighscorer({ enableCcipRead: false }); + }) }); it('mappings with variable-length keys', async () => { @@ -121,20 +158,34 @@ describe('ArbVerifier', () => { enableCcipRead: true, }); expect(result).to.equal('Vitalik Buterin'); + await estimateCCIPReadCallbackGas(provider, ()=>{ + return target.getNickname('Money Skeleton', { + enableCcipRead: false, + }); + }) }); it('nested proofs of mappings with variable-length keys', async () => { const result = await target.getPrimaryNickname({ enableCcipRead: true }); expect(result).to.equal('Hal Finney'); + await estimateCCIPReadCallbackGas(provider, ()=>{ + return target.getPrimaryNickname({ enableCcipRead: false }); + }) }); it('treats uninitialized storage elements as zeroes', async () => { const result = await target.getZero({ enableCcipRead: true }); expect(Number(result)).to.equal(0); + await estimateCCIPReadCallbackGas(provider, ()=>{ + return target.getZero({ enableCcipRead: false }); + }) }); it('treats uninitialized dynamic values as empty strings', async () => { const result = await target.getNickname('Santa', { enableCcipRead: true }); expect(result).to.equal(""); + await estimateCCIPReadCallbackGas(provider, ()=>{ + return target.getNickname('Santa', { enableCcipRead: false }); + }) }) }); diff --git a/bun.lockb b/bun.lockb index 7c6c560a..2fd188d9 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/evm-gateway/package.json b/evm-gateway/package.json index 050e1c2d..4ae0aa48 100644 --- a/evm-gateway/package.json +++ b/evm-gateway/package.json @@ -63,4 +63,4 @@ "type": "module", "types": "./_types/index.d.ts", "typings": "./_types/index.d.ts" -} +} \ No newline at end of file diff --git a/evm-gateway/src/EVMProofHelper.ts b/evm-gateway/src/EVMProofHelper.ts index e18ee0a2..d4395163 100644 --- a/evm-gateway/src/EVMProofHelper.ts +++ b/evm-gateway/src/EVMProofHelper.ts @@ -4,7 +4,7 @@ import { toBeHex, type AddressLike, type JsonRpcProvider } from 'ethers'; * Response of the eth_getProof RPC method. */ interface EthGetProofResponse { - accountProof: string[]; + accountProof: string; balance: string; codeHash: string; nonce: string; @@ -12,13 +12,13 @@ interface EthGetProofResponse { storageProof: { key: string; value: string; - proof: string[]; + proof: string; }[]; } export interface StateProof { - stateTrieWitness: string[]; - storageProofs: string[][]; + stateTrieWitness: string; + storageProofs: string[]; stateRoot: string; } diff --git a/evm-gateway/src/index.ts b/evm-gateway/src/index.ts index b582ddbc..d10c1626 100644 --- a/evm-gateway/src/index.ts +++ b/evm-gateway/src/index.ts @@ -1,4 +1,8 @@ export { EVMGateway, StorageLayout } from './EVMGateway.js'; export { EVMProofHelper, type StateProof } from './EVMProofHelper.js'; export type { IProofService, ProvableBlock } from './IProofService.js'; -export { propsDecoder, type Router } from './utils.js'; +export { + propsDecoder, + convertIntoMerkleTrieProof, + type Router, +} from './utils.js'; diff --git a/evm-gateway/src/utils.ts b/evm-gateway/src/utils.ts index e3975480..79c2315d 100644 --- a/evm-gateway/src/utils.ts +++ b/evm-gateway/src/utils.ts @@ -1,5 +1,7 @@ import { type Request as CFWRequest } from '@cloudflare/workers-types'; import { type PropsDecoder } from '@ensdomains/server-analytics'; +import { AbiCoder } from 'ethers'; +import { type StateProof } from './EVMProofHelper.js'; export interface Router { handle: (request: CFWRequest) => Promise; } @@ -22,3 +24,14 @@ export const propsDecoder: PropsDecoder = ( return {}; } }; + +const flatten = (data: string) => { + return AbiCoder.defaultAbiCoder().encode(['bytes[]'], [data]); +}; + +export const convertIntoMerkleTrieProof = (proof: StateProof) => { + return { + stateTrieWitness: flatten(proof.stateTrieWitness), + storageProofs: proof.storageProofs.map(flatten), + }; +}; diff --git a/evm-verifier/contracts/EVMProofHelper.sol b/evm-verifier/contracts/EVMProofHelper.sol index d91f1396..8d7897a0 100644 --- a/evm-verifier/contracts/EVMProofHelper.sol +++ b/evm-verifier/contracts/EVMProofHelper.sol @@ -6,8 +6,8 @@ import {Bytes} from "@eth-optimism/contracts-bedrock/src/libraries/Bytes.sol"; import {SecureMerkleTrie} from "./SecureMerkleTrie.sol"; struct StateProof { - bytes[] stateTrieWitness; // Witness proving the `storageRoot` against a state root. - bytes[][] storageProofs; // An array of proofs of individual storage elements + 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; @@ -17,52 +17,39 @@ uint8 constant FLAG_DYNAMIC = 0x01; library EVMProofHelper { 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 target address to verify against + * @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/MerkleTrieProofHelper.sol b/evm-verifier/contracts/MerkleTrieProofHelper.sol new file mode 100644 index 00000000..243f6468 --- /dev/null +++ b/evm-verifier/contracts/MerkleTrieProofHelper.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/* Library Imports */ +import { SecureMerkleTrie } from "./SecureMerkleTrie.sol"; +import {RLPReader} from '@eth-optimism/contracts-bedrock/src/libraries/rlp/RLPReader.sol'; + +/** + * @title MerkleTrieProofHelper + * @notice MerkleTrieProofHelper is a helper library that has functions to interact + * with SecureMerkleTrie. + */ +library MerkleTrieProofHelper { + error AccountNotFound(address); + + /* + * @notice Get the storage value for the provided merkle proof + * @param address Unused. Required so the function signature matches the one required by `EVMProofHelper` + * @param slot The storage key we are fetching the value of + * @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){ + (bytes[] memory _witness) = abi.decode(witness, (bytes[])); + + (bool exists, bytes memory retrievedValue) = SecureMerkleTrie.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) { + (bytes[] memory _witness) = abi.decode(witness, (bytes[])); + (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])); + } +} diff --git a/evm-verifier/contracts/SecureMerkleTrie.sol b/evm-verifier/contracts/SecureMerkleTrie.sol index 40211f05..11209c92 100644 --- a/evm-verifier/contracts/SecureMerkleTrie.sol +++ b/evm-verifier/contracts/SecureMerkleTrie.sol @@ -14,28 +14,6 @@ import { MerkleTrie } from "./MerkleTrie.sol"; * keys. Ethereum's state trie hashes input keys before storing them. */ library SecureMerkleTrie { - /** - * @notice Verifies a proof that a given key/value pair is present in the Merkle trie. - * - * @param _key Key of the node to search for, as a hex string. - * @param _value Value of the node to search for, as a hex string. - * @param _proof Merkle trie inclusion proof for the desired node. Unlike traditional Merkle - * trees, this proof is executed top-down and consists of a list of RLP-encoded - * nodes that make a path down to the target node. - * @param _root Known root of the Merkle trie. Used to verify that the included proof is - * correctly constructed. - * - * @return Whether or not the proof is valid. - */ - function verifyInclusionProof( - bytes memory _key, - bytes memory _value, - bytes[] memory _proof, - bytes32 _root - ) internal pure returns (bool) { - bytes memory key = _getSecureKey(_key); - return MerkleTrie.verifyInclusionProof(key, _value, _proof, _root); - } /** * @notice Retrieves the value associated with a given key. diff --git a/l1-gateway/package.json b/l1-gateway/package.json index d0b17221..30fdb82d 100644 --- a/l1-gateway/package.json +++ b/l1-gateway/package.json @@ -71,4 +71,4 @@ "commander": "^11.0.0", "ethers": "^6.7.1" } -} +} \ No newline at end of file diff --git a/l1-gateway/src/L1ProofService.ts b/l1-gateway/src/L1ProofService.ts index 15d5880e..639aa09b 100644 --- a/l1-gateway/src/L1ProofService.ts +++ b/l1-gateway/src/L1ProofService.ts @@ -5,7 +5,11 @@ import { type JsonRpcProvider, } from 'ethers'; -import { EVMProofHelper, type IProofService } from '@ensdomains/evm-gateway'; +import { + EVMProofHelper, + convertIntoMerkleTrieProof, + type IProofService, +} from '@ensdomains/evm-gateway'; import { Block, type JsonRpcBlock } from '@ethereumjs/block'; type RlpObject = Uint8Array | Array; @@ -74,9 +78,9 @@ export class L1ProofService implements IProofService { return AbiCoder.defaultAbiCoder().encode( [ 'tuple(uint256 blockNo, bytes blockHeader)', - 'tuple(bytes[] stateTrieWitness, bytes[][] storageProofs)', + 'tuple(bytes stateTrieWitness, bytes[] storageProofs)', ], - [{ blockNo, blockHeader }, proof] + [{ blockNo, blockHeader }, convertIntoMerkleTrieProof(proof)] ); } } diff --git a/l1-verifier/contracts/L1Verifier.sol b/l1-verifier/contracts/L1Verifier.sol index dd72ac17..9f1b9317 100644 --- a/l1-verifier/contracts/L1Verifier.sol +++ b/l1-verifier/contracts/L1Verifier.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.17; import { IEVMVerifier } from "@ensdomains/evm-verifier/contracts/IEVMVerifier.sol"; import { RLPReader } from "@eth-optimism/contracts-bedrock/src/libraries/rlp/RLPReader.sol"; import { StateProof, EVMProofHelper } from "@ensdomains/evm-verifier/contracts/EVMProofHelper.sol"; +import {MerkleTrieProofHelper} from '@ensdomains/evm-verifier/contracts/MerkleTrieProofHelper.sol'; struct L1WitnessData { uint256 blockNo; @@ -30,6 +31,7 @@ contract L1Verifier is IEVMVerifier { } RLPReader.RLPItem[] memory headerFields = RLPReader.readList(l1Data.blockHeader); bytes32 stateRoot = bytes32(RLPReader.readBytes(headerFields[3])); - return EVMProofHelper.getStorageValues(target, commands, constants, stateRoot, stateProof); + bytes32 storageRoot = MerkleTrieProofHelper.getStorageRoot(stateRoot, target, stateProof.stateTrieWitness); + return EVMProofHelper.getStorageValues(target, MerkleTrieProofHelper.getTrieProof, commands, constants, storageRoot, stateProof.storageProofs); } } diff --git a/op-gateway/src/OPProofService.ts b/op-gateway/src/OPProofService.ts index 7346fcfd..f3009042 100644 --- a/op-gateway/src/OPProofService.ts +++ b/op-gateway/src/OPProofService.ts @@ -1,4 +1,8 @@ -import { EVMProofHelper, type IProofService } from '@ensdomains/evm-gateway'; +import { + EVMProofHelper, + convertIntoMerkleTrieProof, + type IProofService, +} from '@ensdomains/evm-gateway'; import { type JsonRpcBlock } from '@ethereumjs/block'; import { AbiCoder, Contract, JsonRpcProvider, type AddressLike } from 'ethers'; @@ -109,7 +113,7 @@ export class OPProofService implements IProofService { return AbiCoder.defaultAbiCoder().encode( [ 'tuple(uint256 l2OutputIndex, tuple(bytes32 version, bytes32 stateRoot, bytes32 messagePasserStorageRoot, bytes32 latestBlockhash) outputRootProof)', - 'tuple(bytes[] stateTrieWitness, bytes[][] storageProofs)', + 'tuple(bytes stateTrieWitness, bytes[] storageProofs)', ], [ { @@ -123,7 +127,7 @@ export class OPProofService implements IProofService { latestBlockhash: rpcBlock.hash, }, }, - proof, + convertIntoMerkleTrieProof(proof), ] ); } 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..0c2b5c71 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 {MerkleTrieProofHelper} from '@ensdomains/evm-verifier/contracts/MerkleTrieProofHelper.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 = MerkleTrieProofHelper.getStorageRoot(opData.outputRootProof.stateRoot, target, stateProof.stateTrieWitness); + return EVMProofHelper.getStorageValues(target, MerkleTrieProofHelper.getTrieProof, commands, constants, storageRoot, stateProof.storageProofs); } } diff --git a/package.json b/package.json index bd0a9a5a..78309488 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,12 @@ "l1-gateway", "op-gateway", "arb-gateway", + "scroll-gateway", "evm-verifier", "l1-verifier", "op-verifier", - "arb-verifier" + "arb-verifier", + "scroll-verifier" ], "scripts": { "env": "env", diff --git a/scroll-gateway/LICENSE b/scroll-gateway/LICENSE new file mode 100644 index 00000000..92887e55 --- /dev/null +++ b/scroll-gateway/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Nick Johnson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/scroll-gateway/README.md b/scroll-gateway/README.md new file mode 100644 index 00000000..1cfaae94 --- /dev/null +++ b/scroll-gateway/README.md @@ -0,0 +1,42 @@ +# @ensdomains/scroll-gateway + +An instantiation of [evm-gateway](https://github.com/ensdomains/evmgateway/tree/main/evm-gateway) that targets Scroll - that is, it implements a CCIP-Read gateway that generates proofs of contract state on Scroll. + +For a detailed readme and usage instructions, see the [monorepo readme](https://github.com/ensdomains/evmgateway/tree/main). + +To get started, you need to have an RPC URL for both Ethereum Mainnet and Scroll. You also need to provide an L2_ROLLUP address which is the Rollup contract deployed on Mainnet or the Nitro Node. + +## How to use scroll-gateway locally via cloudflare dev env (aka wrangler) + +``` +npm install -g bun +cd scroll-gateway +bun install +touch .dev.vars +## set L2_PROVIDER_URL and SEARCH_URL(the default for sepolia is https://sepolia-rpc.scroll.io) +yarn dev +``` + +## How to deploy scroll-gateway to cloudflare + +``` +cd scroll-gateway +npm install -g wrangler +wrngler login + +wrangler secret put L2_PROVIDER_URL +yarn deploy +``` + +## How to test + +There is currently no local test node. You need to deploy to public testnet and run the test. +1. deploy l2 contract`L2_ETHERSCAN_API_KEY=$L2_ETHERSCAN_API_KEY DEPLOYER_PRIVATE_KEY=$DEPLOYER_PRIVATE_KEY L2_PROVIDER_URL=$L2_PROVIDER_URL npx hardhat deploy --network scrollSepolia` +2. deploy l1 contract. Modify GATEWAY_URLS on 00_scroll_verifier.ts to point to localhost and run `L1_ETHERSCAN_API_KEY=$L1_ETHERSCAN_API_KEY DEPLOYER_PRIVATE_KEY=$DEPLOYER_PRIVATE_KEY L1_PROVIDER_URL=$L1_PROVIDER_URL ROLLUP_ADDRESS=$ROLLUP_ADDRESS npx hardhat deploy --network sepolia` +3. startup gateway server +``` +cd ../evm-gateway +// Add .dev.vars and add L1_PROVIDER_URL, L2_PROVIDER_URL, and L2_ROLLUP +yarn dev +``` +4. run the test `DEPLOYER_PRIVATE_KEY=$DEPLOYER_PRIVATE_KEY L1_PROVIDER_URL=$L1_PROVIDER_URL ROLLUP_ADDRESS=$ROLLUP_ADDRESS yarn test --network sepolia` diff --git a/scroll-gateway/example.env b/scroll-gateway/example.env new file mode 100644 index 00000000..e69de29b diff --git a/scroll-gateway/package.json b/scroll-gateway/package.json new file mode 100644 index 00000000..9f2a37e0 --- /dev/null +++ b/scroll-gateway/package.json @@ -0,0 +1,70 @@ +{ + "name": "@ensdomains/scroll-gateway", + "version": "0.1.0", + "author": "Nick Johnson", + "license": "MIT", + "type": "module", + "main": "./_cjs/index.js", + "module": "./_esm/index.js", + "types": "./_types/index.d.ts", + "typings": "./_types/index.d.ts", + "bin": "./_cjs/server.js", + "sideEffects": false, + "files": [ + "_esm", + "_cjs", + "_types", + "src", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./_types/index.d.ts", + "import": "./_esm/index.js", + "require": "./_cjs/index.js" + }, + "./package.json": "./package.json" + }, + "engines": { + "node": ">=10", + "bun": ">=1.0.4" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + }, + "scripts": { + "start": "bun ./src/server.ts", + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "build:cjs": "tsc --project tsconfig.build.json --module commonjs --outDir ./_cjs --removeComments --verbatimModuleSyntax false && echo > ./_cjs/package.json '{\"type\":\"commonjs\"}'", + "build:esm": "tsc --project tsconfig.build.json --module es2022 --outDir ./_esm && echo > ./_esm/package.json '{\"type\":\"module\",\"sideEffects\":false}'", + "build:types": "tsc --project ./tsconfig.build.json --module esnext --declarationDir ./_types --emitDeclarationOnly --declaration --declarationMap", + "build": "echo 'building scroll-gateway...' && bun run clean && bun run build:cjs && bun run build:esm && bun run build:types", + "prepublishOnly": "bun ../scripts/prepublishOnly.ts", + "lint": "eslint . --ext .ts", + "prepare": "bun run build", + "clean": "rm -fr _cjs _esm _types" + }, + "husky": { + "hooks": { + "pre-commit": "bun run lint" + } + }, + "dependencies": { + "@chainlink/ccip-read-server": "^0.2.1", + "@ensdomains/evm-gateway": "0.1.0-beta.3", + "@ethereumjs/block": "^5.0.0", + "@nomicfoundation/ethereumjs-block": "^5.0.2", + "commander": "^11.0.0", + "ethers": "^6.7.1" + }, + "devDependencies": { + "@commander-js/extra-typings": "^11.0.0" + + } +} diff --git a/scroll-gateway/src/ScrollProofService.ts b/scroll-gateway/src/ScrollProofService.ts new file mode 100644 index 00000000..e332a942 --- /dev/null +++ b/scroll-gateway/src/ScrollProofService.ts @@ -0,0 +1,86 @@ +/* eslint-disable prettier/prettier */ +import { EVMProofHelper, type IProofService } from '@ensdomains/evm-gateway'; +import { AbiCoder, concat, ethers, type AddressLike, } from 'ethers'; + +export interface ScrollProvableBlock { + number: number +} + +/** + * The proofService class can be used to calculate proofs for a given target and slot on the Scroll network. + * It's also capable of proofing long types such as mappings or string by using all included slots in the proof. + * + */ +export class ScrollProofService implements IProofService { + private readonly l2Provider: ethers.JsonRpcProvider; + private readonly helper: EVMProofHelper; + private readonly searchUrl: string; + + constructor( + searchUrl: string, + l2Provider: ethers.JsonRpcProvider, + ) { + this.l2Provider = l2Provider; + this.helper = new EVMProofHelper(l2Provider); + this.searchUrl = searchUrl; + } + + async getStorageAt(block: ScrollProvableBlock, address: AddressLike, slot: bigint): Promise { + return this.helper.getStorageAt(block.number, address, slot); + } + + + /** + * @dev Fetches a set of proofs for the requested state slots. + * @param block A `ProvableBlock` returned by `getProvableBlock`. + * @param address The address of the contract to fetch data from. + * @param slots An array of slots to fetch data for. + * @returns A proof of the given slots, encoded in a manner that this service's + * corresponding decoding library will understand. + */ + async getProofs( + block: ScrollProvableBlock, + address: AddressLike, + slots: bigint[] + ): Promise { + const { batch_index: batchIndex } = await ( + await fetch(`${this.searchUrl}?keyword=${Number(block.number)}`) + ).json(); + const proof = await this.helper.getProofs(Number(block.number), address, slots); + const compressedProofs: string[] = []; + const accountProof: string = proof.stateTrieWitness; + for (let index = 0; index < proof.storageProofs.length; index++) { + const storageProof: string = proof.storageProofs[index]; + compressedProofs[index] = concat([ + `0x${accountProof.length.toString(16).padStart(2, "0")}`, + ...accountProof, + `0x${storageProof.length.toString(16).padStart(2, "0")}`, + ...storageProof, + ]); + } + return AbiCoder.defaultAbiCoder().encode( + [ + 'tuple(uint256 batchIndex)', + 'tuple(bytes[] storageProofs)', + ], + [ + { + batchIndex + }, + { + storageProofs:compressedProofs + }, + ] + ); + } + /** + * @dev Returns an object representing a block whose state can be proven on L1. + */ + public async getProvableBlock(): Promise { + const block = await this.l2Provider.send("eth_getBlockByNumber", ["finalized", false]); + if (!block) throw new Error('No block found'); + return { + number: block.number + }; + } +} diff --git a/scroll-gateway/src/index.ts b/scroll-gateway/src/index.ts new file mode 100644 index 00000000..756f4c89 --- /dev/null +++ b/scroll-gateway/src/index.ts @@ -0,0 +1,18 @@ +import { EVMGateway } from '@ensdomains/evm-gateway'; +import { JsonRpcProvider } from 'ethers'; +import { + ScrollProofService, + type ScrollProvableBlock, +} from './ScrollProofService.js'; + +export type ScrollGateway = EVMGateway; + +export async function makeScrollGateway( + searchUrl: string, + l2providerUrl: string +): Promise { + const l2Provider = new JsonRpcProvider(l2providerUrl); + return new EVMGateway(new ScrollProofService(searchUrl, l2Provider)); +} + +export { ScrollProofService, type ScrollProvableBlock }; diff --git a/scroll-gateway/src/server.ts b/scroll-gateway/src/server.ts new file mode 100644 index 00000000..61dab943 --- /dev/null +++ b/scroll-gateway/src/server.ts @@ -0,0 +1,40 @@ +import { Server } from '@chainlink/ccip-read-server'; +import { Command } from '@commander-js/extra-typings'; +import { EVMGateway } from '@ensdomains/evm-gateway'; +import { JsonRpcProvider } from 'ethers'; +import { ScrollProofService } from './ScrollProofService.js'; + +const program = new Command() + .option('-p, --port ', 'port to listen on', '8080') + .option( + '-v, --l2-provider-url ', + 'l2 provider url', + 'http://localhost:9545/' + ) + .option( + '-s --search-url ', + 'search url to fetch batch index', + process.env.SEARCH_URL + ); + +program.parse(); + +(async () => { + const options = program.opts(); + + const l2Provider = new JsonRpcProvider(options.l2ProviderUrl); + + const gateway = new EVMGateway( + new ScrollProofService(options.searchUrl, l2Provider) + ); + const server = new Server(); + gateway.add(server); + const app = server.makeApp('/'); + + const port = parseInt(options.port); + if (String(port) !== options.port) throw new Error('Invalid port'); + + app.listen(port, function () { + console.log(`Listening on ${port}`); + }); +})(); diff --git a/scroll-gateway/src/worker.ts b/scroll-gateway/src/worker.ts new file mode 100644 index 00000000..871059b6 --- /dev/null +++ b/scroll-gateway/src/worker.ts @@ -0,0 +1,52 @@ +import { Request as CFWRequest } from '@cloudflare/workers-types'; +import { Server } from '@ensdomains/ccip-read-cf-worker'; +import { propsDecoder, type Router } from '@ensdomains/evm-gateway'; +import { Tracker } from '@ensdomains/server-analytics'; + +interface Env { + L1_PROVIDER_URL: string; + L2_PROVIDER_URL: string; + L2_ROLLUP: string; + GATEWAY_DOMAIN: string; + ENDPOINT_URL: string; + SEARCH_URL: string; +} + +let app: Router; + +async function fetch(request: CFWRequest, env: Env) { + const { L2_PROVIDER_URL, GATEWAY_DOMAIN, ENDPOINT_URL, SEARCH_URL } = env; + + // Loading libraries dynamically as a temp work around. + // Otherwise, deployment thorws "Error: Script startup exceeded CPU time limit." error + const tracker = new Tracker(GATEWAY_DOMAIN, { + apiEndpoint: ENDPOINT_URL, + enableLogging: true, + }); + + if (!app) { + const ethers = await import('ethers'); + + const EVMGateway = (await import('@ensdomains/evm-gateway')).EVMGateway; + const ScrollProofService = (await import('./ScrollProofService.js')) + .ScrollProofService; + const l2Provider = new ethers.JsonRpcProvider(L2_PROVIDER_URL); + + const gateway = new EVMGateway( + new ScrollProofService(SEARCH_URL, l2Provider) + ); + + const server = new Server(); + gateway.add(server); + app = server.makeApp('/'); + } + const props = propsDecoder(request); + await tracker.trackEvent(request, 'request', { props }, true); + return app + .handle(request) + .then(tracker.logResult.bind(tracker, propsDecoder, request)); +} + +export default { + fetch, +}; diff --git a/scroll-gateway/tsconfig.build.json b/scroll-gateway/tsconfig.build.json new file mode 100644 index 00000000..7208c6cb --- /dev/null +++ b/scroll-gateway/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "include": ["./src"], + "exclude": ["src/**/*.test.ts", "src/**/*.bench.ts"], + "compilerOptions": { + "sourceMap": true, + "rootDir": "./src", + "moduleResolution": "Node" + } +} diff --git a/scroll-gateway/tsconfig.json b/scroll-gateway/tsconfig.json new file mode 100644 index 00000000..3cf966d7 --- /dev/null +++ b/scroll-gateway/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../tsconfig.json", + "include": ["src"] +} diff --git a/scroll-gateway/wrangler.toml b/scroll-gateway/wrangler.toml new file mode 100644 index 00000000..dd127195 --- /dev/null +++ b/scroll-gateway/wrangler.toml @@ -0,0 +1,15 @@ +name = "scroll-gateway-worker" +account_id = "15dcc9085cb794bb4f29d3e8177ac880" +main = "./src/worker.ts" +node_compat = true +compatibility_date = "2023-10-13" + + +[dev] +port = 8080 + +[build] +command = "yarn build" + +[env.sepolia] +name = "scroll-sepolia-gateway-worker" diff --git a/scroll-verifier/.gitignore b/scroll-verifier/.gitignore new file mode 100644 index 00000000..3360da73 --- /dev/null +++ b/scroll-verifier/.gitignore @@ -0,0 +1,12 @@ +node_modules +.env +coverage +coverage.json +typechain +typechain-types + +# Hardhat files +cache +artifacts +deployments + diff --git a/scroll-verifier/LICENSE b/scroll-verifier/LICENSE new file mode 100644 index 00000000..92887e55 --- /dev/null +++ b/scroll-verifier/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Nick Johnson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/scroll-verifier/README.md b/scroll-verifier/README.md new file mode 100644 index 00000000..e54db085 --- /dev/null +++ b/scroll-verifier/README.md @@ -0,0 +1,88 @@ +# @ensdomains/scroll-verifier + +A complete Solidity library that facilitates sending CCIP-Read requests for Scroll state, and verifying the responses. + +For a detailed readme and usage instructions, see the [monorepo readme](https://github.com/ensdomains/evmgateway/tree/main). + +## Testing + +Start up a devnet by following Scrolls's instructions [here](https://docs.scroll.io/node-running/how-tos/local-dev-node). + +The test requires you to use the rollup address of your node, which may not always be the same. This address is printed in the logs after you've initially set up the node. Copy that value and replace it accordingly. Unfortunately, there is no endpoint to retrieve the rollup address dynamically. + +Copy the rollup address from the Node's Logs. Add it to the following files + +``` +scroll-verifier/test/testScrollVerifier.ts +``` + +``` +scroll-verifier/deploy_l1/00_scroll_verifier.ts +``` + +Build the project + +``` +bun run build +``` + +Open another terminal window and start the Gateway + +``` +cd ./scroll-gateway && bun run start -u http://127.0.0.1:8545/ -v http://127.0.0.1:8547/ -o $ROLLUP_ADDRESS -p 8089 +``` + +Go back to the first Termina window and deploy the contracts to the test node + +``` +npx hardhat --network scrollDevnetL1 deploy && npx hardhat --network scrollDevnetL2 deploy +``` + +Finally, run the tests: + +``` +bun run test +``` + +## Deployments + +### Goerli + +#### L2 + +- TestL2.sol = [0xAdef74372444e716C0473dEe1F9Cb3108EFa3818](https://goerli.scrollscan.dev/address/0xAdef74372444e716C0473dEe1F9Cb3108EFa3818#code) + +#### L1 + +- ScrollVerifier = [0x9E46DeE08Ad370bEFa7858c0E9a6c87f2D7E57A1](https://goerli.etherscan.io/address/0x9E46DeE08Ad370bEFa7858c0E9a6c87f2D7E57A1#code) + +- TestL1.sol = [0x0d6c6B70cd561EB59e6818D832197fFad60840AB](https://goerli.etherscan.io/address/0x0d6c6B70cd561EB59e6818D832197fFad60840AB#code) + +#### Gateway server + +- https://scroll-gateway-worker.ens-cf.workers.dev + + +### Sepolia + +#### L2 + +- TestL2.sol = [0x162A433068F51e18b7d13932F27e66a3f99E6890](https://api-sepolia.scrollscan.dev/address/0x162A433068F51e18b7d13932F27e66a3f99E6890#code) + +#### L1 + +- ScrollVerifier = [0x6820E47CED34D6F275c6d26C3876D48B2c1fdf27](https://sepolia.etherscan.io/address/0x6820E47CED34D6F275c6d26C3876D48B2c1fdf27#code) +- TestL1.sol = [0x50200c7Ccb1abD927184396547ea8dD1A18CAA3A](https://sepolia.etherscan.io/address/0x50200c7Ccb1abD927184396547ea8dD1A18CAA3A#code) + +deploying "ScrollVerifier" (tx: 0x61ae88749f911f1e09d7c073f34a13bb843c71fafaf93a1266423798bd3aadc6)...: deployed at 0x6820E47CED34D6F275c6d26C3876D48B2c1fdf27 with 3872186 gas +deploying "TestL1" (tx: 0x0a7b6b74357d20f33cb89df12da3db34b5cd3c764403888420108ca13f0126fa)...: deployed at 0x50200c7Ccb1abD927184396547ea8dD1A18CAA3A with 2411152 gas + +#### Gateway url + +- https://scroll-sepolia-gateway-worker.ens-cf.workers.dev + +## Testing gateway + +``` +TARGET_ADDRESS=$TEST_L1_ADDRESS PROVIDER_URL=$L1_PROVIDER_URL npx hardhat run ../l1-verifier/scripts/remote.ts --network sepolia +``` \ No newline at end of file diff --git a/scroll-verifier/arguments.js b/scroll-verifier/arguments.js new file mode 100644 index 00000000..2a0c521d --- /dev/null +++ b/scroll-verifier/arguments.js @@ -0,0 +1,6 @@ +module.exports = [ + [ + 'https://scroll-sepolia-gateway-worker.ens-cf.workers.dev/{sender}/{data}.json' + ], + '0xE0BfA7f3B06A9589A914BE09Ba0E5671f481A722' +]; diff --git a/scroll-verifier/contracts/ScrollVerifier.sol b/scroll-verifier/contracts/ScrollVerifier.sol new file mode 100644 index 00000000..5f1e11f6 --- /dev/null +++ b/scroll-verifier/contracts/ScrollVerifier.sol @@ -0,0 +1,85 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; +import {EVMProofHelper} from '@ensdomains/evm-verifier/contracts/EVMProofHelper.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( + address account, + bytes32 storageKey, + bytes calldata proof + ) external view returns (bytes32 stateRoot, bytes32 storageValue); + + function rollup() external view returns (address); + + function verifyStateCommitment( + uint256 batchIndex, + address account, + bytes32 storageKey, + bytes calldata proof + ) external view returns (bytes32 storageValue); +} + +struct ScrollWitnessData { + uint256 batchIndex; +} + +struct StateProof { + bytes[] storageProofs; // An array of proofs of individual storage elements +} + +contract ScrollVerifier is IEVMVerifier { + error InvalidSlotSize(uint256 size); + error StateRootMismatch(bytes32 expected, bytes32 actual); + IScrollChainCommitmentVerifier public immutable verifier; + string[] _gatewayURLs; + + constructor(string[] memory _urls, IScrollChainCommitmentVerifier _verifierAddress) { + verifier = _verifierAddress; + _gatewayURLs = _urls; + } + + /* + * Retrieves an array of gateway URLs used by the contract. + * @returns {string[]} An array containing the gateway URLs. + * */ + function gatewayURLs() external view returns (string[] memory) { + return _gatewayURLs; + } + + + 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); + if(stateRoot != root) { + revert StateRootMismatch(stateRoot, root); + } + return abi.encodePacked(storageValue); + } + + /* + * Retrieves storage values from the specified target address + * + * @param {address} target - The target address from which storage values are to be retrieved. + * @param {bytes32[]} commands - An array of storage keys (commands) to query. + * @param {bytes[]} constants - An array of constant values corresponding to the storage keys. + * @param {bytes} proof - The proof data containing Scroll witness data and state proof. + */ + function getStorageValues( + address target, + bytes32[] memory commands, + bytes[] memory constants, + bytes memory proof + ) external view returns (bytes[] memory values) { + (ScrollWitnessData memory scrollData, StateProof memory stateProof) = abi.decode(proof, (ScrollWitnessData, StateProof)); + bytes32 expectedStateRoot = IScrollChain(verifier.rollup()).finalizedStateRoots(scrollData.batchIndex); + return EVMProofHelper.getStorageValues(target, getTrieProof, commands, constants, expectedStateRoot, stateProof.storageProofs); + } +} diff --git a/scroll-verifier/contracts/test/TestL1.sol b/scroll-verifier/contracts/test/TestL1.sol new file mode 100644 index 00000000..2581aa70 --- /dev/null +++ b/scroll-verifier/contracts/test/TestL1.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import { EVMFetcher } from '@ensdomains/evm-verifier/contracts/EVMFetcher.sol'; +import { EVMFetchTarget } from '@ensdomains/evm-verifier/contracts/EVMFetchTarget.sol'; +import { IEVMVerifier } from '@ensdomains/evm-verifier/contracts/IEVMVerifier.sol'; + +contract TestL1 is EVMFetchTarget { + using EVMFetcher for EVMFetcher.EVMFetchRequest; + + IEVMVerifier verifier; // Slot 0 + address target; + + constructor(IEVMVerifier _verifier, address _target) { + verifier = _verifier; + target = _target; + } + + function getLatest() public view returns(uint256) { + EVMFetcher.newFetchRequest(verifier, target) + .getStatic(0) + .fetch(this.getLatestCallback.selector, ""); + } + + function getLatestCallback(bytes[] memory values, bytes memory) public pure returns(uint256) { + return abi.decode(values[0], (uint256)); + } + + function getName() public view returns(string memory) { + EVMFetcher.newFetchRequest(verifier, target) + .getDynamic(1) + .fetch(this.getNameCallback.selector, ""); + } + + function getNameCallback(bytes[] memory values, bytes memory) public pure returns(string memory) { + return string(values[0]); + } + + function getHighscorer(uint256 idx) public view returns(string memory) { + EVMFetcher.newFetchRequest(verifier, target) + .getDynamic(3) + .element(idx) + .fetch(this.getHighscorerCallback.selector, ""); + } + + function getHighscorerCallback(bytes[] memory values, bytes memory) public pure returns(string memory) { + return string(values[0]); + } + + function getLatestHighscore() public view returns(uint256) { + EVMFetcher.newFetchRequest(verifier, target) + .getStatic(0) + .getStatic(2) + .ref(0) + .fetch(this.getLatestHighscoreCallback.selector, ""); + } + + function getLatestHighscoreCallback(bytes[] memory values, bytes memory) public pure returns(uint256) { + return abi.decode(values[1], (uint256)); + } + + function getLatestHighscorer() public view returns(string memory) { + EVMFetcher.newFetchRequest(verifier, target) + .getStatic(0) + .getDynamic(3) + .ref(0) + .fetch(this.getLatestHighscorerCallback.selector, ""); + } + + function getLatestHighscorerCallback(bytes[] memory values, bytes memory) public pure returns(string memory) { + return string(values[1]); + } + + function getNickname(string memory _name) public view returns(string memory) { + EVMFetcher.newFetchRequest(verifier, target) + .getDynamic(4) + .element(_name) + .fetch(this.getNicknameCallback.selector, ""); + } + + function getNicknameCallback(bytes[] memory values, bytes memory) public pure returns (string memory) { + return string(values[0]); + } + + function getPrimaryNickname() public view returns(string memory) { + EVMFetcher.newFetchRequest(verifier, target) + .getDynamic(1) + .getDynamic(4) + .ref(0) + .fetch(this.getPrimaryNicknameCallback.selector, ""); + } + + function getPrimaryNicknameCallback(bytes[] memory values, bytes memory) public pure returns (string memory) { + return string(values[1]); + } + + function getZero() public view returns(uint256) { + EVMFetcher.newFetchRequest(verifier, target) + .getStatic(5) + .fetch(this.getZeroCallback.selector, ""); + } + + function getZeroCallback(bytes[] memory values, bytes memory) public pure returns (uint256) { + return abi.decode(values[0], (uint256)); + } +} diff --git a/scroll-verifier/contracts/test/TestL2.sol b/scroll-verifier/contracts/test/TestL2.sol new file mode 100644 index 00000000..f5fa3c47 --- /dev/null +++ b/scroll-verifier/contracts/test/TestL2.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +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 + + constructor() { + latest = 42; + name = "Satoshi"; + highscores[latest] = 12345; + highscorers[latest] = "Hal Finney"; + highscorers[1] = "Hubert Blaine Wolfeschlegelsteinhausenbergerdorff Sr."; + realnames["Money Skeleton"] = "Vitalik Buterin"; + realnames["Satoshi"] = "Hal Finney"; + } +} \ No newline at end of file diff --git a/scroll-verifier/deploy_l1/00_scroll_verifier.ts b/scroll-verifier/deploy_l1/00_scroll_verifier.ts new file mode 100644 index 00000000..474253a3 --- /dev/null +++ b/scroll-verifier/deploy_l1/00_scroll_verifier.ts @@ -0,0 +1,38 @@ +import { DeployFunction } from 'hardhat-deploy/types'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import 'dotenv/config'; + +const GATEWAY_URLS = { + scrollDevnetL1: 'http://localhost:8089/{sender}/{data}.json', + goerli: 'https://scroll-gateway-worker.ens-cf.workers.dev/{sender}/{data}.json', + // Point to localhost if you want to test locally + // sepolia: 'http://localhost:8080/{sender}/{data}.json' + sepolia: 'https://scroll-sepolia-gateway-worker.ens-cf.workers.dev/{sender}/{data}.json', +}; + +const ROLLUP_ADDRESSES = { + sepolia: '0xE0BfA7f3B06A9589A914BE09Ba0E5671f481A722', +}; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { deployments, getNamedAccounts, network } = hre; + const { deploy } = deployments; + const { deployer } = await getNamedAccounts(); + let ROLLUP_ADDRESS; + if (network.name === 'scrollDevnetL1') { + //Rollup address according to sequencer config. Unfortunately, there is no endpoint to fetch it at runtime from the rollup. + //The address can be found at nitro-testnode-sequencer-1/config/deployment.json + ROLLUP_ADDRESS = process.env.ROLLUP_ADDRESS; + } else { + ROLLUP_ADDRESS = ROLLUP_ADDRESSES[network.name]; + } + const GATEWAY_URL = GATEWAY_URLS[network.name]; + console.log('ScrollVerifier', [[GATEWAY_URL], ROLLUP_ADDRESS]); + await deploy('ScrollVerifier', { + from: deployer, + args: [[GATEWAY_URL], ROLLUP_ADDRESS], + log: true, + }); +}; +export default func; +func.tags = ['ScrollVerifier']; diff --git a/scroll-verifier/deploy_l1/10_testl1.ts b/scroll-verifier/deploy_l1/10_testl1.ts new file mode 100644 index 00000000..38ec5e8b --- /dev/null +++ b/scroll-verifier/deploy_l1/10_testl1.ts @@ -0,0 +1,22 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { DeployFunction } from 'hardhat-deploy/types'; + + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { deployments, getNamedAccounts } = hre; + const { deploy } = deployments; + + const { deployer } = await getNamedAccounts(); + + const ScrollVerifier = await deployments.get('ScrollVerifier'); + const TestL2 = await hre.companionNetworks['l2'].deployments.get('TestL2'); + + await deploy('TestL1', { + from: deployer, + args: [ScrollVerifier.address, TestL2.address], + log: true, + }); + +}; +export default func; +func.tags = ['TestL1']; diff --git a/scroll-verifier/deploy_l2/01_testl2 .ts b/scroll-verifier/deploy_l2/01_testl2 .ts new file mode 100644 index 00000000..263a9d58 --- /dev/null +++ b/scroll-verifier/deploy_l2/01_testl2 .ts @@ -0,0 +1,17 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { DeployFunction } from 'hardhat-deploy/types'; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { deployments, getNamedAccounts } = hre; + const { deploy } = deployments; + + const { deployer } = await getNamedAccounts(); + + await deploy('TestL2', { + from: deployer, + args: [], + log: true, + }); +}; +export default func; +func.tags = ['TestL2']; diff --git a/scroll-verifier/example.env b/scroll-verifier/example.env new file mode 100644 index 00000000..eb96d04e --- /dev/null +++ b/scroll-verifier/example.env @@ -0,0 +1,2 @@ +DEPLOYER_PRIVATE_KEY ="" +ROLLUP_ADDRESS = "" diff --git a/scroll-verifier/hardhat.config.ts b/scroll-verifier/hardhat.config.ts new file mode 100644 index 00000000..f439e885 --- /dev/null +++ b/scroll-verifier/hardhat.config.ts @@ -0,0 +1,77 @@ +import '@nomicfoundation/hardhat-toolbox'; +import 'hardhat-deploy'; +import 'hardhat-deploy-ethers'; +import { HardhatUserConfig } from 'hardhat/config'; +import 'ethers'; +const DEPLOYER_PRIVATE_KEY = + process.env.DEPLOYER_PRIVATE_KEY || + 'ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'; +const L1_PROVIDER_URL = process.env.L1_PROVIDER_URL || ''; +const L1_ETHERSCAN_API_KEY = process.env.L1_ETHERSCAN_API_KEY; +const L2_ETHERSCAN_API_KEY = process.env.L2_ETHERSCAN_API_KEY; +console.log({ + DEPLOYER_PRIVATE_KEY, + L1_PROVIDER_URL, + L1_ETHERSCAN_API_KEY, + L2_ETHERSCAN_API_KEY +}) +const config: HardhatUserConfig = { + solidity: '0.8.19', + networks: { + scrollDevnetL1: { + url: 'http://127.0.0.1:8545/', + accounts: [ + '0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659', + ], + deploy: ['deploy_l1/'], + companionNetworks: { + l2: 'scrollDevnetL2', + }, + }, + scrollDevnetL2: { + url: 'http://127.0.0.1:8547/', + accounts: [ + '0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659', + ], + deploy: ['deploy_l2/'], + }, + sepolia: { + url: L1_PROVIDER_URL, + accounts: [DEPLOYER_PRIVATE_KEY], + deploy: ['deploy_l1/'], + companionNetworks: { + l2: 'scrollSepolia', + }, + chainId: 11155111 + }, + scrollSepolia: { + url: 'https://sepolia-rpc.scroll.io', + accounts: [DEPLOYER_PRIVATE_KEY], + deploy: [ "deploy_l2/" ], + chainId: 534351 + }, + }, + etherscan: { + apiKey: { + goerli: L1_ETHERSCAN_API_KEY, + sepolia: L1_ETHERSCAN_API_KEY, + scrollGoerli: L2_ETHERSCAN_API_KEY, + scrollSepolia: L2_ETHERSCAN_API_KEY, + }, + customChains: [ + { + network: "scrollSepolia", + chainId: 534351, + urls: { + apiURL: "https://api-sepolia.scrollscan.com/api", + browserURL: "https://sepolia.scrollscan.com" + } + } + ], + }, + namedAccounts: { + deployer: 0, + }, +}; + +export default config; diff --git a/scroll-verifier/package.json b/scroll-verifier/package.json new file mode 100644 index 00000000..48162c18 --- /dev/null +++ b/scroll-verifier/package.json @@ -0,0 +1,43 @@ +{ + "name": "@ensdomains/scroll-verifier", + "license": "MIT", + "version": "0.1.0", + "scripts": { + "build": "echo 'building scroll-verifier...' && bun hardhat compile", + "test": " hardhat test", + "clean": "rm -fr artifacts cache node_modules typechain-types", + "lint": "exit 0" + }, + "devDependencies": { + "@ensdomains/scroll-gateway": "workspace:*", + "@nomicfoundation/hardhat-chai-matchers": "^2.0.0", + "@nomicfoundation/hardhat-ethers": "^3.0.0", + "@nomicfoundation/hardhat-network-helpers": "^1.0.0", + "@nomicfoundation/hardhat-toolbox": "^3.0.0", + "@nomicfoundation/hardhat-verify": "^1.0.0", + "@typechain/ethers-v6": "^0.4.0", + "@typechain/hardhat": "^8.0.0", + "@types/chai": "^4.2.0", + "@types/express": "^4.17.18", + "@types/mocha": ">=9.1.0", + "@types/supertest": "^2.0.14", + "chai": "^4.2.0", + "ethers": "^6.8.0", + "express": "^4.18.2", + "ganache": "^7.9.1", + "hardhat": "^2.16.0", + "hardhat-deploy": "^0.11.43", + "hardhat-deploy-ethers": "^0.4.1", + "hardhat-gas-reporter": "^1.0.8", + "solidity-bytes-utils": "^0.8.0", + "solidity-coverage": "^0.8.1", + "supertest": "^6.3.3", + "ts-node": "^10.9.1", + "typechain": "^8.2.0", + "typescript": "^5.2.2" + }, + "dependencies": { + "@ensdomains/evm-verifier": "0.1.0-beta.3", + "dotenv": "^16.3.1" + } +} diff --git a/scroll-verifier/test/abi/helperAbi.ts b/scroll-verifier/test/abi/helperAbi.ts new file mode 100644 index 00000000..03432fee --- /dev/null +++ b/scroll-verifier/test/abi/helperAbi.ts @@ -0,0 +1,170 @@ +export default [ + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "components": [ + { + "internalType": "bytes32[2]", + "name": "bytes32Vals", + "type": "bytes32[2]" + }, + { + "internalType": "uint64[2]", + "name": "u64Vals", + "type": "uint64[2]" + } + ], + "internalType": "struct GlobalState", + "name": "globalState", + "type": "tuple" + }, + { + "internalType": "enum MachineStatus", + "name": "machineStatus", + "type": "uint8" + } + ], + "internalType": "struct ExecutionState", + "name": "beforeState", + "type": "tuple" + }, + { + "components": [ + { + "components": [ + { + "internalType": "bytes32[2]", + "name": "bytes32Vals", + "type": "bytes32[2]" + }, + { + "internalType": "uint64[2]", + "name": "u64Vals", + "type": "uint64[2]" + } + ], + "internalType": "struct GlobalState", + "name": "globalState", + "type": "tuple" + }, + { + "internalType": "enum MachineStatus", + "name": "machineStatus", + "type": "uint8" + } + ], + "internalType": "struct ExecutionState", + "name": "afterState", + "type": "tuple" + }, + { + "internalType": "uint64", + "name": "numBlocks", + "type": "uint64" + } + ], + "internalType": "struct Assertion", + "name": "assertion", + "type": "tuple" + } + ], + "name": "getBlockHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "hash", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "components": [ + { + "internalType": "bytes32[2]", + "name": "bytes32Vals", + "type": "bytes32[2]" + }, + { + "internalType": "uint64[2]", + "name": "u64Vals", + "type": "uint64[2]" + } + ], + "internalType": "struct GlobalState", + "name": "globalState", + "type": "tuple" + }, + { + "internalType": "enum MachineStatus", + "name": "machineStatus", + "type": "uint8" + } + ], + "internalType": "struct ExecutionState", + "name": "beforeState", + "type": "tuple" + }, + { + "components": [ + { + "components": [ + { + "internalType": "bytes32[2]", + "name": "bytes32Vals", + "type": "bytes32[2]" + }, + { + "internalType": "uint64[2]", + "name": "u64Vals", + "type": "uint64[2]" + } + ], + "internalType": "struct GlobalState", + "name": "globalState", + "type": "tuple" + }, + { + "internalType": "enum MachineStatus", + "name": "machineStatus", + "type": "uint8" + } + ], + "internalType": "struct ExecutionState", + "name": "afterState", + "type": "tuple" + }, + { + "internalType": "uint64", + "name": "numBlocks", + "type": "uint64" + } + ], + "internalType": "struct Assertion", + "name": "assertion", + "type": "tuple" + } + ], + "name": "getSendRoot", + "outputs": [ + { + "internalType": "bytes32", + "name": "root", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + } +] \ No newline at end of file diff --git a/scroll-verifier/test/abi/rollupABI.ts b/scroll-verifier/test/abi/rollupABI.ts new file mode 100644 index 00000000..dc46e714 --- /dev/null +++ b/scroll-verifier/test/abi/rollupABI.ts @@ -0,0 +1,112 @@ +export default [ + { + "inputs": [ + { + "internalType": "address", + "name": "_poseidon", + "type": "address" + }, + { + "internalType": "address", + "name": "_rollup", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "poseidon", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rollup", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "batchIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "storageKey", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "proof", + "type": "bytes" + } + ], + "name": "verifyStateCommitment", + "outputs": [ + { + "internalType": "bytes32", + "name": "storageValue", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "storageKey", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "proof", + "type": "bytes" + } + ], + "name": "verifyZkTrieProof", + "outputs": [ + { + "internalType": "bytes32", + "name": "stateRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "storageValue", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/scroll-verifier/test/testScrollVerifier.ts b/scroll-verifier/test/testScrollVerifier.ts new file mode 100644 index 00000000..e5f871a9 --- /dev/null +++ b/scroll-verifier/test/testScrollVerifier.ts @@ -0,0 +1,186 @@ +import { Server } from '@chainlink/ccip-read-server'; +import { makeScrollGateway } from '@ensdomains/scroll-gateway'; +import { HardhatEthersProvider } from '@nomicfoundation/hardhat-ethers/internal/hardhat-ethers-provider'; +import type { HardhatEthersHelpers } from '@nomicfoundation/hardhat-ethers/types'; +import { expect } from 'chai'; +import { + AbiCoder, + concat, + Contract, + FetchRequest, + Provider, + Signer, + ethers as ethersT +} from 'ethers'; +import express from 'express'; +import hre, { ethers } from 'hardhat'; +import { EthereumProvider } from 'hardhat/types'; +import request from 'supertest'; +const estimateCCIPReadCallbackGas = async (provider, cb) => { + try{ + await cb() + }catch(e){ + const [sender, urls, data, callbackFunction, extraData ] = e.revert.args + const url = `http://localhost:8080/${sender}/${data}.json` + const responseData:any = await (await fetch(url)).json() + const encoder = new AbiCoder() + const encoded = encoder.encode([ "bytes", "bytes" ], [responseData.data, extraData]); + const newdata = concat([ callbackFunction, encoded ]) + const result2 = await provider.estimateGas({ + to: sender, + data:newdata + }); + console.log(`Gas estimate ${result2}`) + } +} + +type ethersObj = typeof ethersT & + Omit & { + provider: Omit & { + _hardhatProvider: EthereumProvider; + }; + }; + +declare module 'hardhat/types/runtime' { + const ethers: ethersObj; + interface HardhatRuntimeEnvironment { + ethers: ethersObj; + } +} + +describe('ScrollVerifier', () => { + let provider: Provider; + let signer: Signer; + let gateway: express.Application; + let target: Contract; + + before(async () => { + // Hack to get a 'real' ethers provider from hardhat. The default `HardhatProvider` + // doesn't support CCIP-read. + provider = new ethers.BrowserProvider(hre.network.provider); + signer = await provider.getSigner(0); + + const rollupAddress = process.env.ROLLUP_ADDRESS; + + const gateway = await makeScrollGateway( + (hre.network.config as any).url, + (hre.config.networks[hre.network.companionNetworks.l2] as any).url, + rollupAddress + ); + const server = new Server() + gateway.add(server) + const app = server.makeApp('/') + + + // Replace ethers' fetch function with one that calls the gateway directly. + const getUrl = FetchRequest.createGetUrlFunc(); + ethers.FetchRequest.registerGetUrl(async (req: FetchRequest) => { + if (req.url != "test:") return getUrl(req); + + const r = request(app).post('/'); + if (req.hasBody()) { + r.set('Content-Type', 'application/json').send( + ethers.toUtf8String(req.body) + ); + } + const response = await r; + return { + statusCode: response.statusCode, + statusMessage: response.ok ? 'OK' : response.statusCode.toString(), + body: ethers.toUtf8Bytes(JSON.stringify(response.body)), + headers: { + 'Content-Type': 'application/json', + }, + }; + }); + + const targetDeployment = await hre.deployments.get('TestL1'); + target = await ethers.getContractAt('TestL1', targetDeployment.address, signer); + }) + + it('simple proofs for fixed values', async () => { + const result = await target.getLatest({ enableCcipRead: true }); + expect(Number(result)).to.equal(42); + await estimateCCIPReadCallbackGas(provider, ()=>{ + return target.getLatest({ enableCcipRead: false }); + }) + }); + + it('simple proofs for dynamic values', async () => { + const result = await target.getName({ enableCcipRead: true }); + expect(result).to.equal('Satoshi'); + await estimateCCIPReadCallbackGas(provider, ()=>{ + return target.getName({ enableCcipRead: false }); + }) + }); + + it('nested proofs for dynamic values', async () => { + const result = await target.getHighscorer(42, { enableCcipRead: true }); + expect(result).to.equal('Hal Finney'); + await estimateCCIPReadCallbackGas(provider, ()=>{ + return target.getHighscorer(42, { enableCcipRead: false }); + }) + }); + + it('nested proofs for long dynamic values', async () => { + const result = await target.getHighscorer(1, { enableCcipRead: true }); + expect(result).to.equal( + 'Hubert Blaine Wolfeschlegelsteinhausenbergerdorff Sr.' + ); + await estimateCCIPReadCallbackGas(provider, ()=>{ + return target.getHighscorer(1, { enableCcipRead: false }); + }) + }); + + it('nested proofs with lookbehind', async () => { + const result = await target.getLatestHighscore({ enableCcipRead: true }); + expect(Number(result)).to.equal(12345); + await estimateCCIPReadCallbackGas(provider, ()=>{ + return target.getLatestHighscore({ enableCcipRead: false }); + }) + }); + + it('nested proofs with lookbehind for dynamic values', async () => { + const result = await target.getLatestHighscorer({ enableCcipRead: true }); + expect(result).to.equal('Hal Finney'); + await estimateCCIPReadCallbackGas(provider, ()=>{ + return target.getLatestHighscorer({ enableCcipRead: false }); + }) + }); + + it('mappings with variable-length keys', async () => { + const result = await target.getNickname('Money Skeleton', { + enableCcipRead: true, + }); + expect(result).to.equal('Vitalik Buterin'); + await estimateCCIPReadCallbackGas(provider, ()=>{ + return target.getNickname('Money Skeleton', { + enableCcipRead: false, + }); + }) + }); + + it('nested proofs of mappings with variable-length keys', async () => { + const result = await target.getPrimaryNickname({ enableCcipRead: true }); + expect(result).to.equal('Hal Finney'); + await estimateCCIPReadCallbackGas(provider, ()=>{ + return target.getPrimaryNickname({ enableCcipRead: false }); + }) + }); + + it('treats uninitialized storage elements as zeroes', async () => { + const result = await target.getZero({ enableCcipRead: true }); + expect(Number(result)).to.equal(0); + await estimateCCIPReadCallbackGas(provider, ()=>{ + return target.getZero({ enableCcipRead: false }); + }) + }); + + it('treats uninitialized dynamic values as empty strings', async () => { + const result = await target.getNickname('Santa', { enableCcipRead: true }); + expect(result).to.equal(""); + await estimateCCIPReadCallbackGas(provider, ()=>{ + return target.getNickname('Santa', { enableCcipRead: false }); + }) + }) +}); diff --git a/scroll-verifier/tsconfig.json b/scroll-verifier/tsconfig.json new file mode 100644 index 00000000..40ec721a --- /dev/null +++ b/scroll-verifier/tsconfig.json @@ -0,0 +1,14 @@ +{ + //"extends": "../tsconfig.json", + "include": ["./scripts", "./test", "../scroll-gateway/src/ScrollProofService.ts"], + "files": ["hardhat.config.ts"], + "compilerOptions": { + "esModuleInterop": true, + "verbatimModuleSyntax": false, + "allowSyntheticDefaultImports": true, + "target": "ES2020", + "module": "CommonJS", + "moduleResolution": "Node", + "skipLibCheck": true + } +}