diff --git a/ibc.ts/.editorconfig b/ibc.ts/.editorconfig index e9149aad0b..8e987400fb 100644 --- a/ibc.ts/.editorconfig +++ b/ibc.ts/.editorconfig @@ -3,3 +3,7 @@ trim_trailing_whitespace = true insert_final_newline = true indent_style = space indent_size = 4 + +[.env.default] +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/ibc.ts/.env.default b/ibc.ts/.env.default index cff08b4f12..05b9ceaa48 100644 --- a/ibc.ts/.env.default +++ b/ibc.ts/.env.default @@ -1,6 +1,12 @@ CHAIN_A_RPC_URL="http://localhost:8080" CHAIN_A_NETWORK_ID="tc" CHAIN_A_FAUCET_ADDRESS="tccqym6znlgc48qeelrzccehkcaut7yz39wwq96q3y7" +CHAIN_A_COUNTERPARTY_CLIENT_ID="BClient" +CHAIN_A_COUNTERPARTY_CONNECTION_ID="BConnection" +CHAIN_A_COUNTERPARTY_CHANNEL_ID="BChannel" CHAIN_B_RPC_URL="http://localhost:8081" CHAIN_B_NETWORK_ID="fc" CHAIN_B_FAUCET_ADDRESS="fccqyd6clszl2aeq4agrk8sgq8whkty6ktljuemc9y3" +CHAIN_B_COUNTERPARTY_CLIENT_ID="AClient" +CHAIN_B_COUNTERPARTY_CONNECTION_ID="AConnection" +CHAIN_B_COUNTERPARTY_CHANNEL_ID="AChannel" diff --git a/ibc.ts/src/common/chain.ts b/ibc.ts/src/common/chain.ts index 1f28bd541e..f76d02f8c6 100644 --- a/ibc.ts/src/common/chain.ts +++ b/ibc.ts/src/common/chain.ts @@ -4,9 +4,26 @@ import { H256, PlatformAddress } from "codechain-primitives"; import { IBC } from "./foundry/transaction"; import { delay } from "./util"; import Debug from "debug"; +import { ClientState } from "./foundry/types"; +import { IBCHeader, IBCQueryResult } from "./types"; const debug = Debug("common:tx"); +export interface CounterpartyIdentifiers { + /** + * Identifier for counter party chain's light client saved in this chain + */ + client: string; + /** + * Identifier for connection with counterparty chain + */ + connection: string; + /** + * Identifier for channel with counterparty chain + */ + channel: string; +} + export interface ChainConfig { /** * Example: "http://localhost:8080" @@ -14,11 +31,13 @@ export interface ChainConfig { server: string; networkId: string; faucetAddress: PlatformAddress; + counterpartyIdentifiers: CounterpartyIdentifiers; } export class Chain { private readonly sdk: SDK; private readonly faucetAddress: PlatformAddress; + public readonly counterpartyIdentifiers: CounterpartyIdentifiers; public constructor(config: ChainConfig) { this.sdk = new SDK({ @@ -26,6 +45,7 @@ export class Chain { networkId: config.networkId }); this.faucetAddress = config.faucetAddress; + this.counterpartyIdentifiers = config.counterpartyIdentifiers; } public async submitDatagram(datagram: Datagram): Promise { @@ -41,6 +61,23 @@ export class Chain { const txHash = await this.sdk.rpc.chain.sendSignedTransaction(signedTx); waitForTx(this.sdk, txHash); } + + public async latestHeight(): Promise { + return await this.sdk.rpc.chain.getBestBlockNumber(); + } + + public async queryClient( + blockNumber: number + ): Promise | null> { + return this.sdk.rpc.sendRpcRequest("ibc_query_client_state", [ + this.counterpartyIdentifiers.client, + blockNumber + ]); + } + + public async queryHeader(blockNumber: number): Promise { + return this.sdk.rpc.sendRpcRequest("ibc_compose_header", [blockNumber]); + } } async function waitForTx(sdk: SDK, txHash: H256) { diff --git a/ibc.ts/src/common/datagram/createClient.ts b/ibc.ts/src/common/datagram/createClient.ts index fb0a513358..150b4adfd6 100644 --- a/ibc.ts/src/common/datagram/createClient.ts +++ b/ibc.ts/src/common/datagram/createClient.ts @@ -28,6 +28,6 @@ export class CreateClientDatagram { } public toEncodeObject(): any[] { - return [this.id, this.kind, this.consensusState, this.data]; + return [1, this.id, this.kind, this.consensusState, this.data]; } } diff --git a/ibc.ts/src/common/datagram/updateClient.ts b/ibc.ts/src/common/datagram/updateClient.ts new file mode 100644 index 0000000000..9ac55094c0 --- /dev/null +++ b/ibc.ts/src/common/datagram/updateClient.ts @@ -0,0 +1,19 @@ +const RLP = require("rlp"); + +export class UpdateClientDatagram { + private id: string; + private header: Buffer; + + public constructor({ id, header }: { id: string; header: Buffer }) { + this.id = id; + this.header = header; + } + + public rlpBytes(): Buffer { + return RLP.encode(this.toEncodeObject()); + } + + public toEncodeObject(): any[] { + return [2, this.id, this.header]; + } +} diff --git a/ibc.ts/src/common/foundry/types.ts b/ibc.ts/src/common/foundry/types.ts new file mode 100644 index 0000000000..f8761a90d6 --- /dev/null +++ b/ibc.ts/src/common/foundry/types.ts @@ -0,0 +1,4 @@ +export interface ClientState { + number: number; + next_validator_set_hash: string; +} diff --git a/ibc.ts/src/common/types.ts b/ibc.ts/src/common/types.ts new file mode 100644 index 0000000000..a969385519 --- /dev/null +++ b/ibc.ts/src/common/types.ts @@ -0,0 +1,6 @@ +export interface IBCQueryResult { + data: T | null; + proof: string; +} + +export type IBCHeader = string; diff --git a/ibc.ts/src/relayer/config.ts b/ibc.ts/src/relayer/config.ts index 18344b0d94..f08210b6d1 100644 --- a/ibc.ts/src/relayer/config.ts +++ b/ibc.ts/src/relayer/config.ts @@ -11,6 +11,9 @@ interface FoundryChainConfig { rpcURL: string; networkId: string; faucetAddress: string; + counterpartyClientId: string; + counterpartyConnectionId: string; + counterpartyChannelId: string; } export function getConfig(): Config { @@ -18,12 +21,22 @@ export function getConfig(): Config { chainA: { rpcURL: getEnv("CHAIN_A_RPC_URL"), networkId: getEnv("CHAIN_A_NETWORK_ID"), - faucetAddress: getEnv("CHAIN_A_FAUCET_ADDRESS") + faucetAddress: getEnv("CHAIN_A_FAUCET_ADDRESS"), + counterpartyClientId: getEnv("CHAIN_A_COUNTERPARTY_CLIENT_ID"), + counterpartyConnectionId: getEnv( + "CHAIN_A_COUNTERPARTY_CONNECTION_ID" + ), + counterpartyChannelId: getEnv("CHAIN_A_COUNTERPARTY_CHANNEL_ID") }, chainB: { rpcURL: getEnv("CHAIN_B_RPC_URL"), networkId: getEnv("CHAIN_B_NETWORK_ID"), - faucetAddress: getEnv("CHAIN_B_FAUCET_ADDRESS") + faucetAddress: getEnv("CHAIN_B_FAUCET_ADDRESS"), + counterpartyClientId: getEnv("CHAIN_B_COUNTERPARTY_CLIENT_ID"), + counterpartyConnectionId: getEnv( + "CHAIN_B_COUNTERPARTY_CONNECTION_ID" + ), + counterpartyChannelId: getEnv("CHAIN_B_COUNTERPARTY_CHANNEL_ID") } }; } diff --git a/ibc.ts/src/relayer/index.ts b/ibc.ts/src/relayer/index.ts index ce55775d92..d477d16d73 100644 --- a/ibc.ts/src/relayer/index.ts +++ b/ibc.ts/src/relayer/index.ts @@ -4,6 +4,7 @@ import { Datagram } from "../common/datagram/index"; import { delay } from "../common/util"; import { getConfig } from "./config"; import { PlatformAddress } from "codechain-primitives/lib"; +import { UpdateClientDatagram } from "../common/datagram/updateClient"; require("dotenv").config(); @@ -14,12 +15,22 @@ async function main() { const chainA = new Chain({ server: config.chainA.rpcURL, networkId: config.chainA.networkId, - faucetAddress: PlatformAddress.fromString(config.chainA.faucetAddress) + faucetAddress: PlatformAddress.fromString(config.chainA.faucetAddress), + counterpartyIdentifiers: { + client: config.chainA.counterpartyClientId, + connection: config.chainA.counterpartyConnectionId, + channel: config.chainA.counterpartyChannelId + } }); const chainB = new Chain({ server: config.chainB.rpcURL, networkId: config.chainB.networkId, - faucetAddress: PlatformAddress.fromString(config.chainB.faucetAddress) + faucetAddress: PlatformAddress.fromString(config.chainB.faucetAddress), + counterpartyIdentifiers: { + client: config.chainB.counterpartyClientId, + connection: config.chainB.counterpartyConnectionId, + channel: config.chainB.counterpartyChannelId + } }); while (true) { @@ -57,8 +68,71 @@ async function relayFromTo({ } } -async function pendingDatagrams( - args: any -): Promise<{ localDatagrams: Datagram[]; counterpartyDatagrams: Datagram[] }> { - return { localDatagrams: [], counterpartyDatagrams: [] }; +async function pendingDatagrams({ + chain, + counterpartyChain +}: { + chain: Chain; + counterpartyChain: Chain; +}): Promise<{ localDatagrams: Datagram[]; counterpartyDatagrams: Datagram[] }> { + const height = await chain.latestHeight(); + const counterpartyChainHeight = await chain.latestHeight(); + let localDatagrams: Datagram[] = []; + let counterpartyDatagrams: Datagram[] = []; + + localDatagrams = localDatagrams.concat( + await updateLightClient({ + chain, + counterpartyChain, + height, + counterpartyChainHeight + }) + ); + + counterpartyDatagrams = counterpartyDatagrams.concat( + await updateLightClient({ + chain: counterpartyChain, + counterpartyChain: chain, + height: counterpartyChainHeight, + counterpartyChainHeight: height + }) + ); + + return { localDatagrams, counterpartyDatagrams }; +} + +async function updateLightClient({ + chain, + counterpartyChain, + height, + counterpartyChainHeight +}: { + chain: Chain; + counterpartyChain: Chain; + height: number; + counterpartyChainHeight: number; +}): Promise { + const datagrams = []; + const clientState = await chain.queryClient(height); + + if (clientState!.data == null) { + throw new Error( + `No client state found. Please create a light client with identifier: ${chain.counterpartyIdentifiers.client}` + ); + } + let currentBlockNumber = clientState!.data!.number; + while (currentBlockNumber < counterpartyChainHeight) { + const header = (await counterpartyChain.queryHeader( + currentBlockNumber + 1 + ))!; + datagrams.push( + new UpdateClientDatagram({ + id: chain.counterpartyIdentifiers.client, + header: Buffer.from(header, "hex") + }) + ); + currentBlockNumber += 1; + } + + return datagrams; }