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 Tests
pull/3367/head
Lee 9 months ago committed by GitHub
parent 332826c5a6
commit 16c0fb10ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 25
      solidity/test/test-data/getProof-data.json
  2. 6
      typescript/ccip-server/.eslintrc
  3. 4
      typescript/ccip-server/.gitignore
  4. 45
      typescript/ccip-server/README.md
  5. 5
      typescript/ccip-server/jest.config.js
  6. 38
      typescript/ccip-server/package.json
  7. 7
      typescript/ccip-server/src/abis/ProofsServiceAbi.ts
  8. 7
      typescript/ccip-server/src/abis/TelepathyCcipReadIsmAbi.ts
  9. 23
      typescript/ccip-server/src/config.ts
  10. 28
      typescript/ccip-server/src/server.ts
  11. 36
      typescript/ccip-server/src/services/HyperlaneService.ts
  12. 99
      typescript/ccip-server/src/services/LightClientService.ts
  13. 149
      typescript/ccip-server/src/services/ProofsService.ts
  14. 47
      typescript/ccip-server/src/services/RPCService.ts
  15. 25
      typescript/ccip-server/src/services/__mocks__/HyperlaneService.ts
  16. 27
      typescript/ccip-server/src/services/__mocks__/LightClientService.ts
  17. 13
      typescript/ccip-server/src/services/__mocks__/RPCService.ts
  18. 8
      typescript/ccip-server/src/services/common/ProofStatusEnum.ts
  19. 18
      typescript/ccip-server/tests/services/HyperlaneService.test.ts
  20. 29
      typescript/ccip-server/tests/services/LightClientService.test.ts
  21. 77
      typescript/ccip-server/tests/services/ProofsService.test.ts
  22. 18
      typescript/ccip-server/tests/services/RPCService.test.ts
  23. 5566
      yarn.lock

@ -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();
});
});

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save