CCIP Gateway Server (#3273)
### Description Gateway Server and Proof Services to create proofs to be consumed by LightClient ISM. See README for more details ### Drive-by changes <!-- Are there any minor or drive-by changes also included? --> ### Related issues Used in #3149 ### Backward compatibility Yes ### Testing Manual/Unit Testspull/3367/head
parent
332826c5a6
commit
16c0fb10ae
@ -0,0 +1,25 @@ |
||||
{ |
||||
"address": "0x3ef546f04a1b24eaf9dce2ed4338a1b5c32e2a56", |
||||
"balance": "0x0", |
||||
"codeHash": "0x2a5a7c6a053518aea9c1affe22c95e2e692204d39c9bdb0826de5012e7c93c4d", |
||||
"nonce": "0x1", |
||||
"storageHash": "0x3f85413bd5bccd8348f123724fb024c5fbab1ab934b0a7485202f75d84803743", |
||||
"accountProof": [ |
||||
"0xf90211a0857923ed0df1a7e89c6c157d9f228ca778b5028cce0fe977bbacdc29ebdcbe1da04cc511d34d843e5ed02112368d4885e8bc25e8e394abbc526459f4e38b842737a07f59950cdd53d4bb97096bbd99e7c04a5d21a97dac8a049f9b308610ea66c98fa02b8ce5f6b67ac79c8947b1919460f7bed77488762c059e5e88c64dfc722aaa56a03521e9571b798e0f86681b9ce0f0e9bf698e9059542d778cc4cde5659a31f9d2a0b90c6a5217bfd233f103dba76e18067f90001297c801cb6feb0c1f7912e0a522a0831099c940ebe93be0a1044566150044d22a8d414518eeb5d919d05468c8f510a054a09da1ca52551a35be75a623e419840060f2e56f86eb3678a689f66b38051da05945b528b186c0162d7cbfa281d30a361ece2a9f1788b33e7fda6bd7c3c5eb07a0401c3ac89520c5a59a19086e82314bb30ad2ecc5ddce09cdf613dd19adb41feca031e6288dc6db773d11cd579b974658473547abfcd82531cf1833484079baff58a099184994c461f1d6ce42e5e91ac66d6c6a5b4cc7ed845f058b795005b46328c1a054bde23ceae3fe9564654f16f867f3f321e0b7e16e3b08e925beb5fb5ffcb1fda040f8d48347ec397a261b090c7e3506f939b6033993c2836c24a487318310da67a0a6e90ed0e95843baeec2c8a6a017e61702556b67ca90c9576342fe268d0a9edba0ca8b4b3ea82cc66299ee45708b555de9b6272d3df24cb472c7baffaa72b39a0d80", |
||||
"0xf90211a0d124c71cf01214346e6facb7cd9e7743fca3a36f4252045573e37dd5b1fc208aa09318652584a16019f80279509f3133ee0f6fed3d391c1c0522b590b923b9283fa0fc90168fe3e76ff17c23781513065c71d13b5f963e492106f2b9a05ed1c9e3dca0c919acc2a76e4fef630534efea6f77a197646db3823ff861c430944385391672a0708cd1310ea8f59c163d0d7b905b456ed377000e989a343dbc61523ec4da6de3a0ade6811b1e1dc547362bfb0cdc943d6ae5d8283252f1e73d3000176a24ed8320a0effada816260fe1f7053c9c6f0cb49e0ee5ebb72be8a05f0cbcad3e7c95dd518a087d09c9bd032f470eed0637da12daa8e6d7c52102d51e87dfdecfdd0f01fc665a0d830bd0ebb1b7d0ddc716138e4341e64fa0c96d4330ac643c3306a842fdfb72ea05fb1c8a24c36a2ccae1ab7c39c0efdf56325dbc5de011f3c548827d592bd7f3ba0e127892391fcf8d6640c45c518ea6f1014b6983586e97e3ec4ae0e546ac22696a0573a0337bba203efa7a2ba3fab1a145e3cfd3b7a70d096489c2c549f9abad184a0daeb93e98ba67210f9e62088838adf7b2e83caa8ec9902e669095066f5abd1f4a0907a2f066f7bad1ebb9ba8998e217c82b2169336daa0f4c0db3b8ac05ad59fc5a03ac759b9a285ceb7293f13071d838261585bb87ae6a8c5489d13b7a818a60e8ea042abb78fd5f125d0dfd6ea10da83f064ed39920c3616e97b7810ee1f306559ec80", |
||||
"0xf90211a0316755c60ab5ac6d1ca0ce7fc7131544cb41bb5907b6c44b0659731c5196f505a0f4d87833b3b7fdb40011cd61f5e4fddf82a99e195e15a6650310d59f276111aba04c923074395b960cd08d04910a3c14ec3cb7d1ae858f9aef5cc888e6e03fbfb8a07c9d54b33c8a9db1e3efe12d0adb042d741fa2f2bccab2bf7d0afcb03626fba8a066400964da5b44e27fadbac20d48cfafc511c3c6bfaa68c1b92ae215e914f61ca038e0e323e92c7a20aaf13c302a493a5241c52a2c169aaaf8d48d23e17f44f643a0646a80b8ac762550cf167377f0837a5993536789eb065369829fd3b13bb52cc5a0de5c001063a09ee8b228199fcd393226a04ac68bc38f600729f7af2305c10368a035cb84ac8c8bb1ddb01e3377a7a17e22e23de20c89f4fa1bd7997bdc0421516aa01a598f35bf00309ed051c4096b8aac157d3411013211b2df2a02a8246eb5d0b6a0fab8ac4cedece1fe1ba89c617413a8df9a28e287e32f92bc52db1485ebb2396ea036f69b2bdc01e9b90102d5df53f9c02f3474db4f613f91d13c46f341b9fb9746a0d0bda7e73681df8b78903454ae252ebd8d34e48af88aa0ad18d6c2fb86a99d89a00d98621a3d3071cc04c492fd2c666637f27138eea11b83f516467f33a00b5eefa0198f566c5350232b08192dfe9cb7dd0a07b21c15638a80f912114a2b5c96be59a035092d3d51e3cd65ad2dcdb2d6c19aadb63a944ac9cedb7093ea68be58b7d83e80", |
||||
"0xf90211a0a7aea3e55e49172a7da1f511fceb247072cb0b4d8a87d9cd5cde0bc87046665ca08a057cd8f49b6ad4841f8e23b81706b8a424eb092663e4e1e60f98dba1e07e44a064098aa80e0d7e101f4e87fa5983f9a43ff6c26d8adaafea0c8f745e0fb9099aa09a5ec75b929e80a80ae115a1aec2827dcdb3eec116fce7a805eebe02d51ff3d7a027c8ef5bfc576ad5e7577005716a18d16ae203c440d65e12a9581a252b9f0e4ba01d352d5770faf1ab8a8096739876d9af10846effbd5f0a4bbb3a53bcb687fc9ca0a9da86853b0a80f1be569b6563aa35e8ab5d635132836b9dc95952b83192bdd3a0aa42135324b35a446ec3620ab361bb98d4636ebd2774715f142550b66cb5c723a0cc9734326b403f694d73ee6dd774e56c5675428ebf24b2ec28b319146f0d1ebda0a0d2c0664e654776405127a978a6edf6c249a71e94747158b2cae156b795b9eaa07a5811d019afead667944b567e595b7ef6084036be9bfde82097a479f3b9d01ca009e0186fe2bbb6f5f8399fabcbda45902405241f25b34352ff7ec66c78fa124aa0cacc8baa6b996e057fe8abaa433f50ba5f9b7afd501a1dbb8b952371e4b604efa081e453659b14017d3f3492fecfa4e9b3a03b8b8d3236fd2c3082c9abf528b6aca030e4ddd56b0b072e820f1afd7bd32cf5cf06f92d69231cbdc236a968c1f43283a0961d902b6b6ee68fb1d75a633a76786a7783f7603f8812ada01f865639e9daf880", |
||||
"0xf90211a045376ea7070c6b6bba71480a083c5ee916a4c72aa89ddc0539f69ca2f951ce96a0786afe5e76d004a35a17fb7931e70f40b0ce3a57e5246c94ebc4629e0a61e5bca0ce44e7e569e415dc667a71d212d260023fee99415bd0bcfd1e5aeee1cdbd8ec9a051cabbabc99fbd009590821598f342f3c2606a4241c81c46dd7e65c18d9ee098a0dbae505b1ce907ff5d451c5d113736ed911b239897d1f6887d3350da314f174ba047f6b90756987d0268044b85bc44da5a34e988044014a6c27cf62d185c7b46e9a0f7566eb4d5c9bc2361ecfffe8fc6a5ac5b5b9380c8b8152284cd714d44e79b12a0cc4a7ee9c4e83d67d64a9bfb0aec93cc3b8966c235b5b65f17333832b023a464a00b9024cfba692f2385d58ea3dca746f9e062982fb21f6f16ead2cefb70d25657a0d187348c2d25747788855d8094ce6e3df6940e26dca20cec4cc26e4f2292ee67a0355474e9a646331b74d171dd023e68f7c50f4bc1257a6433ee01a9ea8ce1577aa096f7bfa110a35e95127808a58ec44f3478b8b67f98ff3b8b140a3e9d249a6deba0dac2dea453ebb23f8a6a8a82db61ca160792aa967db905d98ee0dbcb2b81ef88a0bf7bac1cc4c2e30b47c5bc62ef9aedba995e46ad0432cf78cc8ec064ebf62ec7a0a0b931ee3c50545a4f91864c9501fb02eaa879ab02e4f677b11a1667d42cd55ca0654f679d8743df36bb61fe7eecca444d1a1773435b876fcafadd36c1c9bfe24980", |
||||
"0xf901d1a0c6d1570250983b95bd625c871d0627cc5bf6557738904ec34f6ce54c4d4782dba098975098243c73d0de47c1e3edf85d54a2010b50e85ffdac2192cc71484993e0a028e68d0c580151049649c5c882ddc4fa3f946f832b6fe34f210420f82ad18e0c80a0a0c6abc7c7b8c738ddb43a72c19c6a0dc4eee7808e4653d2ac67d5d777c5cfe8a0a3843b012a7d6170298a6e0024a79e466614296d4d09a72876be8049294d7dcfa02370c2a2892d2eacc15cdc46cef02125499df7afe41a16af6ce78d5fb8d5a7b9a0e9c8fa9b7269050b18b76ac5f07847e93e2403a438783e27bb69bb89484a7a52a08a7fef1f6d80c56ae256362f9de34169d3d66b17a8f0be6dbd0fe82c31a0c4da80a081b05ed42fc65a32afabe8f3ab58eedc8658472f383150e4c631ce1b3a36bacda04fe5deb98851372cb69ab014dba185f1c488ccd9810b4998d7737d0bee884718a0c8d739dcf5707542a9f751ec6a0acb228f28e55659c35973ee8a46aefa9a7a4fa0f0128b39c576d4e23e8180b154d7e8f7031a7d6775191188c87cc11573730008a0d391bf99084848ebff2c014653ec6f9634c80b1115850174cdf0edf83669b073a031d14af58dae3d2fdf510a3c48e988b860d1bfe406e6365be92252d63f4c316080", |
||||
"0xf8679e20bc9ed3e4003248a3f81d89ecc98e14f0359535a5195a1d33a6fe19556bb846f8440180a03f85413bd5bccd8348f123724fb024c5fbab1ab934b0a7485202f75d84803743a02a5a7c6a053518aea9c1affe22c95e2e692204d39c9bdb0826de5012e7c93c4d" |
||||
], |
||||
"storageProof": [ |
||||
{ |
||||
"key": "0x02c1eed75677f1bd39cc3abdd3042974bf12ab4a12ecc40df73fe3aa103e5e0e", |
||||
"proof": [ |
||||
"0xf844a120443dd0be11dd8e645a2e5675fd62011681443445ea8b04c77d2cdeb1326739eca1a031ede38d2e93c5aee49c836f329a626d8c6322abfbff3783e82e5759f870d7e9" |
||||
], |
||||
"value": "0x31ede38d2e93c5aee49c836f329a626d8c6322abfbff3783e82e5759f870d7e9" |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,6 @@ |
||||
{ |
||||
"rules": { |
||||
"no-console": ["off"] |
||||
} |
||||
} |
||||
|
@ -0,0 +1,4 @@ |
||||
.env* |
||||
/dist |
||||
/cache |
||||
/configs |
@ -0,0 +1,45 @@ |
||||
# CCIP-read service framework |
||||
|
||||
This package contains the service framework for the CCIP-read project, built off of the [CCIP-server framework](https://github.com/smartcontractkit/ccip-read). It allows building of any execution logic, given a Hyperlane Relayer call. |
||||
|
||||
# Definitions |
||||
|
||||
- Server: The main entry point, and refers to `server.ts`. |
||||
- Service: A class that handles all logic for a particular service, e.g. ProofService, RPCService, etc. |
||||
- Service ABI: The interface for a service that tells the Server what input and output to expect. It serves similar functionalities as the Solidity ABIs, i.e., used for encoding and decoding data. |
||||
|
||||
# Usage |
||||
|
||||
The Relayer will make a POST request to the Server with a request body similar to the following: |
||||
|
||||
```json |
||||
{ |
||||
"data": "0x0ee9bb2f000000000000000000000000873afca0319f5c04421e90e882566c496877aff8000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001a2d9059b6d822aa460229510c754e9ecec100bb9f649186f5c7d4da8edf59858", |
||||
"sender": "0x4a679253410272dd5232b3ff7cf5dbb88f295319" |
||||
} |
||||
``` |
||||
|
||||
The `data` property will be ABI-encoded, and server will parse it according to the Service ABI. It then will call the handler function with the parsed input. |
||||
|
||||
# Building a Service |
||||
|
||||
1. Create a Service ABI for your Service. This ABI tells the Server how to parse the incoming `data`, and how to encode the output. See `/abi/ProofsServiceAbi.ts` for an example. |
||||
2. Create a new Service class to handle your logic. This should inherit from `HandlerDescriptionEnumerated` if a function will be used to handle a Server request. The handler function should return a Promise that resolves to the output of the Service. See `/service/ProofsService.ts` for examples. |
||||
3. Instantiate the new Service in `server.ts`. For example: |
||||
|
||||
```typescript |
||||
const proofsService = new ProofsService( |
||||
config.LIGHT_CLIENT_ADDR, |
||||
config.RPC_ADDRESS, |
||||
config.STEP_FN_ID, |
||||
config.CHAIN_ID, |
||||
config.SUCCINCT_PLATFORM_URL, |
||||
config.SUCCINCT_API_KEY, |
||||
); |
||||
``` |
||||
|
||||
4. Add the new Service by calling `server.add(...)` by providing the Service ABI, and the handler function. For example: |
||||
|
||||
```typescript |
||||
server.add(ProofsServiceAbi, [proofsService.handler('getProofs')]); |
||||
``` |
@ -0,0 +1,5 @@ |
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */ |
||||
module.exports = { |
||||
preset: 'ts-jest', |
||||
testEnvironment: 'node', |
||||
}; |
@ -0,0 +1,38 @@ |
||||
{ |
||||
"name": "@hyperlane-xyz/ccip-server", |
||||
"version": "0.0.0", |
||||
"description": "CCIP server", |
||||
"typings": "dist/index.d.ts", |
||||
"typedocMain": "src/index.ts", |
||||
"private": true, |
||||
"files": [ |
||||
"src" |
||||
], |
||||
"engines": { |
||||
"node": ">=14" |
||||
}, |
||||
"scripts": { |
||||
"start": "ts-node src/server.ts", |
||||
"dev": "nodemon src/server.ts", |
||||
"test": "jest", |
||||
"prettier": "prettier --write ./src/* ./tests/" |
||||
}, |
||||
"author": "brolee", |
||||
"license": "Apache-2.0", |
||||
"devDependencies": { |
||||
"@jest/globals": "^29.7.0", |
||||
"@types/node": "^16.9.1", |
||||
"jest": "^29.7.0", |
||||
"nodemon": "^3.0.3", |
||||
"prettier": "^2.8.8", |
||||
"ts-jest": "^29.1.2", |
||||
"ts-node": "^10.8.0", |
||||
"typescript": "5.1.6" |
||||
}, |
||||
"dependencies": { |
||||
"@chainlink/ccip-read-server": "^0.2.1", |
||||
"dotenv-flow": "^4.1.0", |
||||
"ethers": "5.7.2", |
||||
"hyperlane-explorer": "https://github.com/hyperlane-xyz/hyperlane-explorer.git" |
||||
} |
||||
} |
@ -0,0 +1,7 @@ |
||||
// This is the ABI for the ProofsService.
|
||||
// This is used to 1) Select the function 2) encode output
|
||||
const ProofsServiceAbi = [ |
||||
'function getProofs(address, bytes32, uint256) public view returns (string[][])', |
||||
]; |
||||
|
||||
export { ProofsServiceAbi }; |
@ -0,0 +1,7 @@ |
||||
const TelepathyCcipReadIsmAbi = [ |
||||
'function verify(bytes, bytes) public view returns (bool)', |
||||
'function step(uint256) external', |
||||
'function syncCommitteePoseidons(uint256) external view returns (bytes32)', |
||||
]; |
||||
|
||||
export { TelepathyCcipReadIsmAbi }; |
@ -0,0 +1,23 @@ |
||||
import dotenvFlow from 'dotenv-flow'; |
||||
|
||||
dotenvFlow.config(); |
||||
|
||||
const RPC_ADDRESS = process.env.RPC_ADDRESS as string; |
||||
const LIGHT_CLIENT_ADDR = process.env.LIGHT_CLIENT_ADDR as string; |
||||
const STEP_FN_ID = process.env.STEP_FN_ID as string; |
||||
const CHAIN_ID = process.env.CHAIN_ID as string; |
||||
const SUCCINCT_PLATFORM_URL = process.env.SUCCINCT_PLATFORM_URL as string; |
||||
const SUCCINCT_API_KEY = process.env.SUCCINCT_API_KEY as string; |
||||
const SERVER_PORT = process.env.SERVER_PORT as string; |
||||
const SERVER_URL_PREFIX = process.env.SERVER_URL_PREFIX as string; |
||||
|
||||
export { |
||||
RPC_ADDRESS, |
||||
LIGHT_CLIENT_ADDR, |
||||
STEP_FN_ID, |
||||
CHAIN_ID, |
||||
SUCCINCT_PLATFORM_URL, |
||||
SUCCINCT_API_KEY, |
||||
SERVER_PORT, |
||||
SERVER_URL_PREFIX, |
||||
}; |
@ -0,0 +1,28 @@ |
||||
import { Server } from '@chainlink/ccip-read-server'; |
||||
|
||||
import { ProofsServiceAbi } from './abis/ProofsServiceAbi'; |
||||
import * as config from './config'; |
||||
import { ProofsService } from './services/ProofsService'; |
||||
|
||||
// Initalize Services
|
||||
const proofsService = new ProofsService( |
||||
config.LIGHT_CLIENT_ADDR, |
||||
config.RPC_ADDRESS, |
||||
config.STEP_FN_ID, |
||||
config.CHAIN_ID, |
||||
config.SUCCINCT_PLATFORM_URL, |
||||
config.SUCCINCT_API_KEY, |
||||
); |
||||
|
||||
// Initalize Server and add Service handlers
|
||||
const server = new Server(); |
||||
|
||||
server.add(ProofsServiceAbi, [ |
||||
{ type: 'getProofs', func: proofsService.getProofs.bind(this) }, |
||||
]); |
||||
|
||||
// Start Server
|
||||
const app = server.makeApp(config.SERVER_URL_PREFIX); |
||||
app.listen(config.SERVER_PORT, () => |
||||
console.log(`Listening on port ${config.SERVER_PORT}`), |
||||
); |
@ -0,0 +1,36 @@ |
||||
import { info } from 'console'; |
||||
import { Message, MessageTx } from 'hyperlane-explorer/src/types'; |
||||
|
||||
// These types are copied from hyperlane-explorer. TODO: export them so this file can use them directly.
|
||||
interface ApiResult<R> { |
||||
status: '0' | '1'; |
||||
message: string; |
||||
result: R; |
||||
} |
||||
|
||||
enum API_ACTION { |
||||
GetMessages = 'get-messages', |
||||
} |
||||
|
||||
class HyperlaneService { |
||||
constructor(readonly baseUrl: string) {} |
||||
|
||||
/** |
||||
* Makes a request to the Explorer API to get the block info by message Id. Throws if request fails, or no results |
||||
* @param id: Message id to look up |
||||
*/ |
||||
async getOriginBlockByMessageId(id: string): Promise<MessageTx> { |
||||
info(`Fetching block for id: ${id}`); |
||||
const response = await fetch( |
||||
`${this.baseUrl}?module=message&action=${API_ACTION.GetMessages}&id=${id}`, |
||||
); |
||||
const responseAsJson: ApiResult<Message[]> = await response.json(); |
||||
if (responseAsJson.status === '1') { |
||||
return responseAsJson.result[0]?.origin; |
||||
} else { |
||||
throw new Error(responseAsJson.message); |
||||
} |
||||
} |
||||
} |
||||
|
||||
export { HyperlaneService }; |
@ -0,0 +1,99 @@ |
||||
import { ethers, utils } from 'ethers'; |
||||
|
||||
import { TelepathyCcipReadIsmAbi } from '../abis/TelepathyCcipReadIsmAbi'; |
||||
|
||||
import { ProofStatus } from './common/ProofStatusEnum'; |
||||
|
||||
export type SuccinctConfig = { |
||||
readonly lightClientAddress: string; |
||||
readonly stepFunctionId: string; |
||||
readonly platformUrl: string; |
||||
readonly apiKey: string; |
||||
}; |
||||
|
||||
// Service that interacts with the LightClient/ISM
|
||||
class LightClientService { |
||||
constructor( |
||||
private readonly lightClientContract: ethers.Contract, // TODO USE TYPECHAIN
|
||||
private succinctConfig: SuccinctConfig, |
||||
) {} |
||||
|
||||
private getSyncCommitteePeriod(slot: bigint): bigint { |
||||
return slot / 8192n; // Slots Per Period
|
||||
} |
||||
|
||||
/** |
||||
* Gets syncCommitteePoseidons from ISM/LightClient |
||||
* @param slot |
||||
* @returns |
||||
*/ |
||||
async getSyncCommitteePoseidons(slot: bigint): Promise<string> { |
||||
return await this.lightClientContract.syncCommitteePoseidons( |
||||
this.getSyncCommitteePeriod(slot), |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Calculates the slot given a timestamp, and the LightClient's configured Genesis Time and Secods Per Slot |
||||
* @param timestamp timestamp to calculate slot with |
||||
*/ |
||||
async calculateSlot(timestamp: bigint): Promise<bigint> { |
||||
return ( |
||||
(timestamp - (await this.lightClientContract.GENESIS_TIME())) / |
||||
(await this.lightClientContract.SECONDS_PER_SLOT()) |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Request the proof from Succinct. |
||||
* @param slot |
||||
* @param syncCommitteePoseidon |
||||
*/ |
||||
async requestProof( |
||||
syncCommitteePoseidon: string, |
||||
slot: bigint, |
||||
): Promise<string> { |
||||
console.log(`Requesting proof for${slot}`); |
||||
|
||||
// Note that Succinct will asynchronously call step() on the ISM/LightClient
|
||||
const telepathyIface = new utils.Interface(TelepathyCcipReadIsmAbi); |
||||
|
||||
const body = { |
||||
chainId: this.lightClientContract.chainId, |
||||
to: this.lightClientContract.address, |
||||
data: telepathyIface.encodeFunctionData('step', [slot]), |
||||
functionId: this.lightClientContract.stepFunctionId, |
||||
input: utils.defaultAbiCoder.encode( |
||||
['bytes32', 'uint64'], |
||||
[syncCommitteePoseidon, slot], |
||||
), |
||||
retry: true, |
||||
}; |
||||
|
||||
const response = await fetch( |
||||
`${this.lightClientContract.platformUrl}/new`, |
||||
{ |
||||
headers: { |
||||
'Content-Type': 'application/json', |
||||
Authorization: `Bearer ${this.succinctConfig.apiKey}`, |
||||
}, |
||||
body: JSON.stringify(body), |
||||
}, |
||||
); |
||||
const responseAsJson = await response.json(); |
||||
|
||||
return responseAsJson.proof_id; |
||||
} |
||||
|
||||
// @dev in the case of when a proof doesn't exist, the request returns an object of { error: 'failed to get proof' }.
|
||||
// Example: GET https://alpha.succinct.xyz/api/proof/4dfd2802-4edf-4c4f-91db-b2d05eb69791
|
||||
async getProofStatus(proofId: string): Promise<ProofStatus> { |
||||
const response = await fetch( |
||||
`${this.lightClientContract.platformUrl}/${proofId}`, |
||||
); |
||||
const responseAsJson = await response.json(); |
||||
return responseAsJson.status ?? ProofStatus.error; |
||||
} |
||||
} |
||||
|
||||
export { LightClientService, ProofStatus }; |
@ -0,0 +1,149 @@ |
||||
import { ethers } from 'ethers'; |
||||
|
||||
import { TelepathyCcipReadIsmAbi } from '../abis/TelepathyCcipReadIsmAbi'; |
||||
|
||||
import { HyperlaneService } from './HyperlaneService'; |
||||
import { LightClientService, SuccinctConfig } from './LightClientService'; |
||||
import { RPCService } from './RPCService'; |
||||
import { ProofResult } from './RPCService'; |
||||
import { ProofStatus } from './common/ProofStatusEnum'; |
||||
|
||||
type RPCConfig = { |
||||
readonly url: string; |
||||
readonly chainId: string; |
||||
}; |
||||
|
||||
type HyperlaneConfig = { |
||||
readonly url: string; |
||||
}; |
||||
|
||||
// Service that requests proofs from Succinct and RPC Provider
|
||||
class ProofsService { |
||||
// Maps from pendingProofKey to pendingProofId
|
||||
pendingProof = new Map<string, string>(); |
||||
|
||||
// External Services
|
||||
rpcService: RPCService; |
||||
lightClientService: LightClientService; |
||||
hyperlaneService: HyperlaneService; |
||||
|
||||
constructor( |
||||
succinctConfig: Required<SuccinctConfig>, |
||||
rpcConfig: Required<RPCConfig>, |
||||
hyperlaneConfig: Required<HyperlaneConfig>, |
||||
) { |
||||
this.rpcService = new RPCService(rpcConfig.url); |
||||
const lightClientContract = new ethers.Contract( |
||||
succinctConfig.lightClientAddress, |
||||
TelepathyCcipReadIsmAbi, |
||||
this.rpcService.provider, |
||||
); |
||||
|
||||
this.lightClientService = new LightClientService( |
||||
lightClientContract, |
||||
succinctConfig, |
||||
); |
||||
|
||||
this.hyperlaneService = new HyperlaneService(hyperlaneConfig.url); |
||||
} |
||||
|
||||
/** |
||||
* Requests the Succinct proof, state proof, and returns account and storage proof |
||||
* @dev Upon requesting Succinct Proof, this function will revert to force the relayer to re-check the pending proof |
||||
* @param target contract address to get the proof for |
||||
* @param storageKeys storage keys to get the proof for |
||||
* @param messageId messageId that will be used to get the block info from hyperlane |
||||
* @returns The account and a single storage proof |
||||
*/ |
||||
async getProofs([ |
||||
target, |
||||
storageKey, |
||||
messageId, |
||||
]: ethers.utils.Result): Promise<Array<[string[], string[]]>> { |
||||
const proofs: Array<[string[], string[]]> = []; |
||||
const pendingProofKey = this.getPendingProofKey( |
||||
target, |
||||
storageKey, |
||||
messageId, |
||||
); |
||||
if (!this.pendingProof.has(pendingProofKey)) { |
||||
// Request a Proof from Succinct
|
||||
const pendingProofId = await this.requestProofFromSuccinct(messageId); |
||||
this.pendingProof.set(pendingProofKey, pendingProofId); |
||||
this.forceRelayerRecheck(); |
||||
} else { |
||||
// Proof is being generated, check status
|
||||
const proofStatus = await this.lightClientService.getProofStatus( |
||||
this.pendingProof.get(pendingProofKey)!, |
||||
); |
||||
if (proofStatus === ProofStatus.success) { |
||||
// Succinct Proof is ready.
|
||||
// This means that the LightClient should have the latest state root. Fetch and return the storage proofs from eth_getProof
|
||||
proofs.push(await this.getStorageProofs(target, storageKey, messageId)); |
||||
this.pendingProof.delete(pendingProofKey); |
||||
} else { |
||||
this.forceRelayerRecheck(); |
||||
} |
||||
} |
||||
// TODO Write tests to check proofs
|
||||
return proofs; |
||||
} |
||||
|
||||
/** |
||||
* Requests the Succinct proof |
||||
* @param messageId messageId that will be used to get the block info from hyperlane |
||||
* @returns the proofId |
||||
*/ |
||||
async requestProofFromSuccinct(messageId: string) { |
||||
const { timestamp } = await this.hyperlaneService.getOriginBlockByMessageId( |
||||
messageId, |
||||
); |
||||
const slot = await this.lightClientService.calculateSlot(BigInt(timestamp)); |
||||
const syncCommitteePoseidon = ''; // TODO get from LC
|
||||
return await this.lightClientService.requestProof( |
||||
syncCommitteePoseidon, |
||||
slot, |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Gets the account and single storage proof from eth_getProof |
||||
* @param target contract address to get the proof for |
||||
* @param storageKeys storage keys to get the proof for |
||||
* @param messageId messageId that will be used to get the block info from hyperlane |
||||
* @returns The account and a single storage proof |
||||
*/ |
||||
async getStorageProofs( |
||||
target: string, |
||||
storageKey: string, |
||||
messageId: string, |
||||
): Promise<[string[], string[]]> { |
||||
const { blockNumber } = |
||||
await this.hyperlaneService.getOriginBlockByMessageId(messageId); |
||||
const { accountProof, storageProof }: ProofResult = |
||||
await this.rpcService.getProofs( |
||||
target, |
||||
[storageKey], |
||||
new Number(blockNumber).toString(16), // Converts to hexstring
|
||||
); |
||||
|
||||
return [accountProof, storageProof[0].proof]; // Since we only expect one storage key, we only return the first proof
|
||||
} |
||||
|
||||
getPendingProofKey( |
||||
target: string, |
||||
storageKey: string, |
||||
messageId: string, |
||||
): string { |
||||
return ethers.utils.defaultAbiCoder.encode( |
||||
['string', 'string', 'string'], |
||||
[target, storageKey, messageId], |
||||
); |
||||
} |
||||
|
||||
forceRelayerRecheck(): void { |
||||
throw new Error('Proof is not ready'); |
||||
} |
||||
} |
||||
|
||||
export { ProofsService }; |
@ -0,0 +1,47 @@ |
||||
import { ethers } from 'ethers'; |
||||
|
||||
type ProofResultStorageProof = { |
||||
key: string; |
||||
proof: Array<string>; |
||||
value: string; |
||||
}; |
||||
|
||||
type ProofResult = { |
||||
accountProof: Array<string>; |
||||
storageProof: Array<ProofResultStorageProof>; |
||||
address: string; |
||||
balance: string; |
||||
codeHash: string; |
||||
nonce: string; |
||||
storageHash: string; |
||||
}; |
||||
|
||||
class RPCService { |
||||
provider: ethers.providers.JsonRpcProvider; |
||||
constructor(private readonly providerAddress: string) { |
||||
this.provider = new ethers.providers.JsonRpcProvider(this.providerAddress); |
||||
} |
||||
|
||||
/** |
||||
* Request state proofs using eth_getProofs |
||||
* @param address |
||||
* @param storageKeys |
||||
* @param block |
||||
* @returns |
||||
*/ |
||||
async getProofs( |
||||
address: string, |
||||
storageKeys: string[], |
||||
block: string, |
||||
): Promise<ProofResult> { |
||||
const results = await this.provider.send('eth_getProof', [ |
||||
address, |
||||
storageKeys, |
||||
block, |
||||
]); |
||||
|
||||
return results; |
||||
} |
||||
} |
||||
|
||||
export { RPCService, ProofResult }; |
@ -0,0 +1,25 @@ |
||||
import { MessageTx } from 'hyperlane-explorer/src/types'; |
||||
|
||||
class HyperlaneService { |
||||
async getOriginBlockByMessageId(id: string): Promise<MessageTx> { |
||||
return { |
||||
timestamp: 123456789, |
||||
hash: '0x123abc456def789', |
||||
from: '0x9876543210abcdef', |
||||
to: '0xabcdef0123456789', |
||||
blockHash: '0x456789abc123def', |
||||
blockNumber: 12345, |
||||
mailbox: '0xabcdef0123456789', |
||||
nonce: 0, |
||||
gasLimit: 1000000, |
||||
gasPrice: 100, |
||||
effectiveGasPrice: 90, |
||||
gasUsed: 50000, |
||||
cumulativeGasUsed: 1234567, |
||||
maxFeePerGas: 150, |
||||
maxPriorityPerGas: 100, |
||||
}; |
||||
} |
||||
} |
||||
|
||||
export { HyperlaneService }; |
@ -0,0 +1,27 @@ |
||||
// TODO figure out why I cannot import this from LightClientService.
|
||||
enum ProofStatus { |
||||
running = 'running', |
||||
success = 'success', |
||||
error = 'error', |
||||
} |
||||
|
||||
class LightClientService { |
||||
proofStatus: ProofStatus = ProofStatus.running; |
||||
async calculateSlot(timestamp: bigint): Promise<bigint> { |
||||
return ( |
||||
(timestamp - 1606824023n) / 12n // (timestamp - GENESIS TIME) / SLOTS_PER_SECOND
|
||||
); |
||||
} |
||||
|
||||
async requestProof( |
||||
syncCommitteePoseidon: string, |
||||
slot: BigInt, |
||||
): Promise<string> { |
||||
return 'pendingProofId12'; |
||||
} |
||||
async getProofStatus(pendingProofId: string): Promise<ProofStatus> { |
||||
return ProofStatus.success; |
||||
} |
||||
} |
||||
|
||||
export { LightClientService }; |
@ -0,0 +1,13 @@ |
||||
import ETH_GET_PROOFS from '../../../../../solidity/test/test-data/getProof-data.json'; |
||||
|
||||
class RPCService { |
||||
getProofs = async ( |
||||
address: string, |
||||
storageKeys: string[], |
||||
block: string, |
||||
): Promise<any> => { |
||||
return ETH_GET_PROOFS; |
||||
}; |
||||
} |
||||
|
||||
export { RPCService }; |
@ -0,0 +1,8 @@ |
||||
// Needs to be in its own file because Mocks will mock the entire file
|
||||
enum ProofStatus { |
||||
running = 'running', |
||||
success = 'success', |
||||
error = 'error', |
||||
} |
||||
|
||||
export { ProofStatus }; |
@ -0,0 +1,18 @@ |
||||
import { describe, expect, test } from '@jest/globals'; |
||||
|
||||
import { HyperlaneService } from '../../src/services/HyperlaneService'; |
||||
|
||||
describe('HyperlaneServiceTest', () => { |
||||
let hyperlaneService: HyperlaneService; |
||||
beforeEach(() => { |
||||
hyperlaneService = new HyperlaneService( |
||||
'https://explorer.hyperlane.xyz/api', |
||||
); |
||||
}); |
||||
test('should get the block by messageId', async () => { |
||||
await hyperlaneService.getOriginBlockByMessageId( |
||||
'0xb0430e396f4014883c01bb3ee43df17ce93d8257a0a0b5778d9d3229a1bf02bb', |
||||
); |
||||
expect(true).toBe(true); |
||||
}); |
||||
}); |
@ -0,0 +1,29 @@ |
||||
import { describe, expect, jest, test } from '@jest/globals'; |
||||
import { ethers } from 'ethers'; |
||||
|
||||
import { TelepathyCcipReadIsmAbi } from '../../src/abis/TelepathyCcipReadIsmAbi'; |
||||
import { LightClientService } from '../../src/services/LightClientService'; |
||||
import { RPCService } from '../../src/services/RPCService'; |
||||
|
||||
describe('LightClientService', () => { |
||||
let lightClientService: LightClientService; |
||||
beforeEach(() => { |
||||
const rpcService = new RPCService('http://localhost:8545'); |
||||
const lightClientContract = new ethers.Contract( |
||||
'lightClientAddress', |
||||
TelepathyCcipReadIsmAbi, |
||||
rpcService.provider, |
||||
); |
||||
lightClientService = new LightClientService(lightClientContract, { |
||||
lightClientAddress: ethers.constants.AddressZero, |
||||
stepFunctionId: ethers.constants.HashZero, |
||||
platformUrl: 'http://localhost:8080', |
||||
apiKey: 'apiKey', |
||||
}); |
||||
|
||||
jest.resetModules(); |
||||
}); |
||||
test('should return the correct proof status', () => { |
||||
expect(lightClientService.calculateSlot(1n)).toBeGreaterThan(0); |
||||
}); |
||||
}); |
@ -0,0 +1,77 @@ |
||||
import { describe, expect, jest, test } from '@jest/globals'; |
||||
import { ethers } from 'ethers'; |
||||
|
||||
// import { LightClientService } from '../../src/services/LightClientService';
|
||||
import { ProofsService } from '../../src/services/ProofsService'; |
||||
|
||||
// Fixtures
|
||||
jest.mock('../../src/services/HyperlaneService'); |
||||
jest.mock('../../src/services/LightClientService'); |
||||
jest.mock('../../src/services/RPCService'); |
||||
|
||||
describe('ProofsService', () => { |
||||
const TARGET_ADDR = 'targetAddress'; |
||||
const MESSAGE_ID = 'msgId'; |
||||
const STORAGE_KEY = ethers.utils.formatBytes32String('10'); |
||||
let proofsService: ProofsService; |
||||
let pendingProofKey: string; |
||||
|
||||
beforeEach(() => { |
||||
proofsService = new ProofsService( |
||||
{ |
||||
lightClientAddress: ethers.constants.AddressZero, |
||||
stepFunctionId: ethers.constants.HashZero, |
||||
platformUrl: 'http://localhost:8080', |
||||
apiKey: 'apiKey', |
||||
}, |
||||
{ |
||||
url: 'http://localhost:8545', |
||||
chainId: '1337', |
||||
}, |
||||
{ |
||||
url: 'http://localhost:8545', |
||||
}, |
||||
); |
||||
pendingProofKey = proofsService.getPendingProofKey( |
||||
TARGET_ADDR, |
||||
STORAGE_KEY, |
||||
MESSAGE_ID, |
||||
); |
||||
jest.clearAllMocks(); |
||||
}); |
||||
|
||||
test('should set currentProofId, if proof is not ready', async () => { |
||||
try { |
||||
await proofsService.getProofs([TARGET_ADDR, STORAGE_KEY, MESSAGE_ID]); |
||||
} catch (e) { |
||||
expect(proofsService.pendingProof.get(pendingProofKey)).toEqual( |
||||
'pendingProofId12', |
||||
); |
||||
} |
||||
}); |
||||
|
||||
test('should reset currentProofId, if proof is ready', async () => { |
||||
const pendingProofKey = proofsService.getPendingProofKey( |
||||
TARGET_ADDR, |
||||
STORAGE_KEY, |
||||
MESSAGE_ID, |
||||
); |
||||
try { |
||||
await proofsService.getProofs([TARGET_ADDR, STORAGE_KEY, MESSAGE_ID]); |
||||
expect(proofsService.pendingProof.get(pendingProofKey)).toEqual( |
||||
'pendingProofId12', |
||||
); |
||||
} catch (e) { |
||||
// Try to get the proof again
|
||||
const proofs = await proofsService.getProofs([ |
||||
TARGET_ADDR, |
||||
STORAGE_KEY, |
||||
MESSAGE_ID, |
||||
]); |
||||
expect(proofs[0][1]).toEqual([ |
||||
'0xf844a120443dd0be11dd8e645a2e5675fd62011681443445ea8b04c77d2cdeb1326739eca1a031ede38d2e93c5aee49c836f329a626d8c6322abfbff3783e82e5759f870d7e9', |
||||
]); |
||||
expect(proofsService.pendingProof.get(pendingProofKey)).toBeUndefined(); |
||||
} |
||||
}); |
||||
}); |
@ -0,0 +1,18 @@ |
||||
import { describe, expect, test } from '@jest/globals'; |
||||
|
||||
import * as config from '../../src/config'; |
||||
import { RPCService } from '../../src/services/RPCService'; |
||||
|
||||
describe('RPCService', () => { |
||||
const rpcService = new RPCService(config.RPC_ADDRESS); |
||||
|
||||
test('should return the proofs from api', async () => { |
||||
const proofs = await rpcService.getProofs( |
||||
'0x3ef546f04a1b24eaf9dce2ed4338a1b5c32e2a56', |
||||
['0x02c1eed75677f1bd39cc3abdd3042974bf12ab4a12ecc40df73fe3aa103e5e0e'], |
||||
'0x1221E88', |
||||
); |
||||
|
||||
expect(proofs).not.toBeNull(); |
||||
}); |
||||
}); |
Loading…
Reference in new issue