Skip to content

Commit

Permalink
Merge branch 'ccip-server' into telepathy-ism
Browse files Browse the repository at this point in the history
  • Loading branch information
ltyu committed Feb 15, 2024
2 parents 8165e78 + 0238992 commit 798652b
Show file tree
Hide file tree
Showing 19 changed files with 2,828 additions and 128 deletions.
4 changes: 4 additions & 0 deletions typescript/ccip-server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.env*
/dist
/cache
/configs
45 changes: 45 additions & 0 deletions typescript/ccip-server/README.md
Original file line number Diff line number Diff line change
@@ -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')]);
```
5 changes: 5 additions & 0 deletions typescript/ccip-server/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
38 changes: 38 additions & 0 deletions typescript/ccip-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "ccip-server",
"version": "0.0.1",
"description": "CCIP server",
"typings": "dist/index.d.ts",
"typedocMain": "src/index.ts",
"files": [
"src"
],
"engines": {
"node": ">=14"
},
"scripts": {
"start": "ts-node src/server.ts",
"dev": "nodemon src/server.ts",
"test": "jest"
},
"author": "brolee",
"license": "MIT",
"devDependencies": {
"@types/jest": "^29.5.11",
"@types/node": "^15.12.2",
"@types/sinon": "^17",
"eslint-plugin-prettier": "^3.4.0",
"jest": "^29.7.0",
"nodemon": "^3.0.3",
"prettier": "^2.3.2",
"sinon": "^17.0.1",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"typescript": "4.8.4"
},
"dependencies": {
"@chainlink/ccip-read-server": "^0.2.1",
"dotenv-flow": "^4.1.0",
"ethers": "5.7.2"
}
}
7 changes: 7 additions & 0 deletions typescript/ccip-server/src/abis/ProofsServiceAbi.ts
Original file line number Diff line number Diff line change
@@ -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 };
7 changes: 7 additions & 0 deletions typescript/ccip-server/src/abis/TelepathyCcipReadIsmAbi.ts
Original file line number Diff line number Diff line change
@@ -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 };
23 changes: 23 additions & 0 deletions typescript/ccip-server/src/config.ts
Original file line number Diff line number Diff line change
@@ -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,
};
26 changes: 26 additions & 0 deletions typescript/ccip-server/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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, [proofsService.handler('getProofs')]);

// Start Server
const app = server.makeApp(config.SERVER_URL_PREFIX);
app.listen(config.SERVER_PORT, () =>
console.log(`Listening on port ${config.SERVER_PORT}`),
);
90 changes: 90 additions & 0 deletions typescript/ccip-server/src/services/LightClientService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// @ts-nocheck
import axios from 'axios';
import { ethers, utils } from 'ethers';

import { TelepathyCcipReadIsmAbi } from '../abis/TelepathyCcipReadIsmAbi';

import { Requestor } from './common/Requestor';

enum ProofStatus {
running = 'running',
success = 'success',
}

// Service that interacts with the LightClient/ISM
class LightClientService extends Requestor {
// Stores the current ProofId that is being generated. Clears once proof is ready.
pendingProofId: string;

constructor(
private readonly lightClientContract: ethers.Contract,
private readonly stepFunctionId: string,
private readonly chainId: string,
readonly platformUrl: string,
readonly platformApiKey: string,
) {
super(axios, platformApiKey);
}

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),
);
}

/**
* Request the proof from Succinct.
* @param slot
* @param syncCommitteePoseidon
*/
async requestProof(syncCommitteePoseidon: string, slot: BigInt) {
if (!this.pendingProofId) {
// Request a Proof, set pendingProofId
// Note that Succinct will asynchronously call step() on the ISM/LightClient
const telepathyIface = new utils.Interface(TelepathyCcipReadIsmAbi);

const body = {
chainId: this.chainId,
to: this.lightClientContract.address,
data: telepathyIface.encodeFunctionData('step', [slot]),
functionId: this.stepFunctionId,
input: utils.defaultAbiCoder.encode(
['bytes32', 'uint64'],
[syncCommitteePoseidon, slot],
),
retry: true,
};

const results: { proof_id: string } = await this.postWithAuthorization(
`${this.platformUrl}/new`,
body,
);
this.pendingProofId = results.proof_id;

// Proof is being generated. Force the Relayer to re-check.
throw new Error('Proof is not ready');
} else {
// Proof is being generated, check status
const proofResults: { status: ProofStatus } = await this.get(
`${this.platformUrl}/${this.pendingProofId}`,
);
if (proofResults.status === ProofStatus.success) {
// Proof is ready, clear pendingProofId
this.pendingProofId = null;
}
// Proof is not ready. Force the Relayer to re-check.
throw new Error('Proof is not ready');
}
}
}

export { LightClientService, ProofStatus };
72 changes: 72 additions & 0 deletions typescript/ccip-server/src/services/ProofsService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { ethers } from 'ethers';

import { TelepathyCcipReadIsmAbi } from '../abis/TelepathyCcipReadIsmAbi';

import { LightClientService } from './LightClientService';
import { ProofResult, RPCService } from './RPCService';
import { HandlerDescriptionEnumerated } from './common/HandlerDescriptionEnumerated';

// Service that requests proofs from Succinct and RPC Provider
class ProofsService extends HandlerDescriptionEnumerated {
rpcService: RPCService;
lightClientService: LightClientService;

constructor(
readonly lightClientAddress: string,
readonly rpcAddress: string,
readonly stepFunctionId: string,
readonly chainId: string,
readonly succinctPlatformUrl: string,
readonly succinctPlatformApiKey: string,
) {
super();
this.rpcService = new RPCService(rpcAddress);
const lightClientContract = new ethers.Contract(
lightClientAddress,
TelepathyCcipReadIsmAbi,
this.rpcService.provider,
);
this.lightClientService = new LightClientService(
lightClientContract,
stepFunctionId,
chainId,
succinctPlatformUrl,
succinctPlatformApiKey,
);
}

/**
* Requests the Succinct proof, state proof, and returns account and storage proof
* @dev Note that the abi encoding will happen within ccip-read-server
* @param target contract address to get the proof for
* @param storageKeys storage keys to get the proof for
* @param blockNumber block to get the proof for. Will decode as a BigInt.
* Note that JS BigInt can only handle 2^53 - 1. For block number, this should be plenty.
*/
async getProofs([
address,
storageKey,
blockNumber,
]: ethers.utils.Result): Promise<Array<[string[], string[]]>> {
const proofs: Array<[string[], string[]]> = [];
try {
// TODO Implement request Proof from Succinct
// await this.lightClientService.requestProof(syncCommitteePoseidon, slot);

// Get storage proofs
const { accountProof, storageProof }: ProofResult =
await this.rpcService.getProofs(
address,
[storageKey],
blockNumber.toHexString(),
);
proofs.push([accountProof, storageProof[0].proof]);
} catch (e) {
console.log('Error getting proofs', e);
}

return proofs;
}
}

export { ProofsService };
47 changes: 47 additions & 0 deletions typescript/ccip-server/src/services/RPCService.ts
Original file line number Diff line number Diff line change
@@ -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 };
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { HandlerDescription, HandlerFunc } from '@chainlink/ccip-read-server';

// Abstract class used to create HandlerDescriptions from a class.
abstract class HandlerDescriptionEnumerated {
handler<K extends keyof this>(func: K): HandlerDescription {
if (typeof this[func] == 'function') {
return {
type: func as string,
func: (this[func] as HandlerFunc).bind(this),
};
}

throw Error(`Invalid function name: ${func.toString()}`);
}
}

export { HandlerDescriptionEnumerated };
Loading

0 comments on commit 798652b

Please sign in to comment.