Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

CCIP Gateway Server #3273

Merged
merged 13 commits into from
Mar 5, 2024
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
ltyu marked this conversation as resolved.
Show resolved Hide resolved
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.
ltyu marked this conversation as resolved.
Show resolved Hide resolved

# 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 @@
{
ltyu marked this conversation as resolved.
Show resolved Hide resolved
"name": "ccip-server",
ltyu marked this conversation as resolved.
Show resolved Hide resolved
"version": "0.0.1",
ltyu marked this conversation as resolved.
Show resolved Hide resolved
"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",
ltyu marked this conversation as resolved.
Show resolved Hide resolved
"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
ltyu marked this conversation as resolved.
Show resolved Hide resolved
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
ltyu marked this conversation as resolved.
Show resolved Hide resolved
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
ltyu marked this conversation as resolved.
Show resolved Hide resolved
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,
ltyu marked this conversation as resolved.
Show resolved Hide resolved
private readonly stepFunctionId: string,
private readonly chainId: string,
readonly platformUrl: string,
readonly platformApiKey: string,
ltyu marked this conversation as resolved.
Show resolved Hide resolved
) {
super(axios, platformApiKey);
}

private getSyncCommitteePeriod(slot: BigInt): BigInt {
return slot / 8192n; // Slots Per Period
ltyu marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Gets syncCommitteePoseidons from ISM/LightClient
* @param slot
* @returns
*/
async getSyncCommitteePoseidons(slot: BigInt): Promise<string> {
ltyu marked this conversation as resolved.
Show resolved Hide resolved
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) {
ltyu marked this conversation as resolved.
Show resolved Hide resolved
// Request a Proof, set pendingProofId
// Note that Succinct will asynchronously call step() on the ISM/LightClient
const telepathyIface = new utils.Interface(TelepathyCcipReadIsmAbi);
ltyu marked this conversation as resolved.
Show resolved Hide resolved

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,
) {
ltyu marked this conversation as resolved.
Show resolved Hide resolved
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);
ltyu marked this conversation as resolved.
Show resolved Hide resolved

// 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
ltyu marked this conversation as resolved.
Show resolved Hide resolved
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 };
ltyu marked this conversation as resolved.
Show resolved Hide resolved
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
Loading