Remove unused classes and tests, Add ConsensusService

ltyu/sp1-lightclient-ism
Le Yu 4 months ago
parent 98e09ae0e2
commit 2b63e14fcb
  1. 2
      solidity/contracts/interfaces/ccip-gateways/ISuccinctProofsService.sol
  2. 5
      typescript/ccip-server/jest.config.js
  3. 2
      typescript/ccip-server/src/abis/ProofsServiceAbi.ts
  4. 7
      typescript/ccip-server/src/abis/TelepathyCcipReadIsmAbi.ts
  5. 10
      typescript/ccip-server/src/config.ts
  6. 27
      typescript/ccip-server/src/server.ts
  7. 9
      typescript/ccip-server/src/services/ConsensusService.ts
  8. 37
      typescript/ccip-server/src/services/HyperlaneService.ts
  9. 99
      typescript/ccip-server/src/services/LightClientService.ts
  10. 143
      typescript/ccip-server/src/services/ProofsService.ts
  11. 60
      typescript/ccip-server/src/services/explorerTypes.ts
  12. 18
      typescript/ccip-server/tests/services/HyperlaneService.test.ts
  13. 29
      typescript/ccip-server/tests/services/LightClientService.test.ts

@ -17,6 +17,6 @@ interface ISuccinctProofsService {
function getProofs(
address target,
bytes32 storageKey,
bytes32 slot
uint256 slot
) external view returns (string[][] memory proofs);
}

@ -1,5 +0,0 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};

@ -1,7 +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 target, bytes32 storageKey, bytes32 slot) public view returns (string[][])',
'function getProofs(address target, bytes32 storageKey, uint256 slot) public view returns (string[][])',
];
export { ProofsServiceAbi };

@ -1,7 +0,0 @@
const TelepathyCcipReadIsmAbi = [
'function verify(bytes, bytes) public view returns (bool)',
'function step(uint256) external',
'function syncCommitteePoseidons(uint256) external view returns (bytes32)',
];
export { TelepathyCcipReadIsmAbi };

@ -3,21 +3,15 @@ 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 CONSENSUS_API_URL = process.env.CONSENSUS_API_URL 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,
CONSENSUS_API_URL,
CHAIN_ID,
SUCCINCT_PLATFORM_URL,
SUCCINCT_API_KEY,
SERVER_PORT,
SERVER_URL_PREFIX,
};

@ -1,28 +1,25 @@
import { Server } from '@chainlink/ccip-read-server';
import { log } from 'console';
import { ProofsServiceAbi } from './abis/ProofsServiceAbi';
import * as config from './config';
import { ProofsService } from './services/ProofsService';
// Initialize 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,
);
// Initialize Server and add Service handlers
const server = new Server();
server.add(ProofsServiceAbi, [
{ type: 'getProofs', func: proofsService.getProofs.bind(this) },
{
type: 'getProofs',
func: async ([target, storageKey, slot]) => {
const proofsService = new ProofsService(
config.RPC_ADDRESS,
config.CONSENSUS_API_URL,
);
return proofsService.getProofs(target, storageKey, slot);
},
},
]);
// Start Server
const app = server.makeApp(config.SERVER_URL_PREFIX);
app.listen(config.SERVER_PORT, () =>
console.log(`Listening on port ${config.SERVER_PORT}`),
log(`Listening on port ${config.SERVER_PORT}`),
);

@ -0,0 +1,9 @@
export class ConsensusService {
constructor(private readonly consensusApiUrl: string) {}
async getOriginBlockNumberBySlot(slot: string): Promise<number> {
const response = await fetch(`${this.consensusApiUrl}/${slot}`);
const responseAsJson = await response.json();
return responseAsJson.data.message.body.execution_payload.block_number;
}
}

@ -1,37 +0,0 @@
import { info } from 'console';
import { Message, MessageTx } from './explorerTypes';
// 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 };

@ -1,99 +0,0 @@
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 Seconds 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 };

@ -1,148 +1,37 @@
import { ethers } from 'ethers';
import { ConsensusService } from './ConsensusService';
import { ProofResult, RPCService } from './RPCService';
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,
);
consensusService: ConsensusService;
this.hyperlaneService = new HyperlaneService(hyperlaneConfig.url);
constructor(rpcUrl: Required<string>, consensusApiUrl: Required<string>) {
this.rpcService = new RPCService(rpcUrl);
this.consensusService = new ConsensusService(consensusApiUrl);
}
/**
* 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
* Requests the account and storage proofs for a given storage key and slot
* @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
* @param slot slot that will be used to get the block info from Consensus API
* @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(
async getProofs(
target: string,
storageKey: string,
messageId: string,
): Promise<[string[], string[]]> {
const { blockNumber } =
await this.hyperlaneService.getOriginBlockByMessageId(messageId);
slot: string,
): Promise<Array<[string[], string[]]>> {
const blockNumber = await this.consensusService.getOriginBlockNumberBySlot(
slot,
);
const { accountProof, storageProof }: ProofResult =
await this.rpcService.getProofs(
target,
[storageKey],
new Number(blockNumber).toString(16), // Converts to hexstring
`0x` + 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');
return [[accountProof, storageProof[0].proof]]; // Since we only expect one storage key, we only return the first proof
}
}

@ -1,60 +0,0 @@
// TODO de-dupe this types with the Explorer by moving them to a shared lib
// These were originally imported from the explorer package but there were two issues
// 1. The explorer is not structured to be a lib (it's an app)
// 2. The explorer's deps on monorepo packages created circular deps leading to transitive deps conflicts
type Address = string;
export enum MessageStatus {
Unknown = 'unknown',
Pending = 'pending',
Delivered = 'delivered',
Failing = 'failing',
}
export interface MessageTxStub {
timestamp: number;
hash: string;
from: Address;
}
export interface MessageTx extends MessageTxStub {
to: Address;
blockHash: string;
blockNumber: number;
mailbox: Address;
nonce: number;
gasLimit: number;
gasPrice: number;
effectiveGasPrice: number;
gasUsed: number;
cumulativeGasUsed: number;
maxFeePerGas: number;
maxPriorityPerGas: number;
}
export interface MessageStub {
status: MessageStatus;
id: string; // Database id
msgId: string; // Message hash
nonce: number; // formerly leafIndex
sender: Address;
recipient: Address;
originChainId: number;
originDomainId: number;
destinationChainId: number;
destinationDomainId: number;
origin: MessageTxStub;
destination?: MessageTxStub;
isPiMsg?: boolean;
}
export interface Message extends MessageStub {
body: string;
decodedBody?: string;
origin: MessageTx;
destination?: MessageTx;
totalGasAmount?: string;
totalPayment?: string;
numPayments?: number;
}

@ -1,18 +0,0 @@
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);
});
});

@ -1,29 +0,0 @@
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);
});
});
Loading…
Cancel
Save