Messenger relayer (#22)

* init

* Configuration to connect spy to wormhole local validator

* plugin + scaffold written

* it works

* cleanup config

* more cleanup

* fix spy_guardiand.bash hostname

* depend on github relayer engine

* clean up
This commit is contained in:
Joe Howarth 2022-10-13 12:02:24 -05:00 committed by GitHub
parent ec13c1d180
commit 7232b5e4a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 11760 additions and 1851 deletions

11439
projects/messenger/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -8,15 +8,19 @@
"dependencies": {
"@certusone/wormhole-sdk": "^0.6.0",
"@project-serum/anchor": "^0.25.0",
"@solana/web3.js": "^1.64.0",
"@types/node-fetch": "^2.6.2",
"byteify": "^2.0.10",
"commander": "^9.4.0",
"ethers": "^5.6.9",
"keccak256": "^1.0.6",
"node-fetch": "2",
"ts-node": "^10.9.1"
"relayer-engine": "wormhole-foundation/relayer-engine#relayer-engine-top-package",
"winston": "^3.8.2"
},
"devDependencies": {
"@types/node": "^18.6.4"
"@types/node": "^18.6.4",
"ts-node": "^10.9.1",
"typescript": "^4.8.4"
}
}

View File

@ -0,0 +1,182 @@
import {
ActionExecutor,
CommonPluginEnv,
ContractFilter,
Plugin,
PluginFactory,
Providers,
StagingArea,
Workflow,
} from "relayer-plugin-interface";
import * as whSdk from "@certusone/wormhole-sdk";
import { Logger } from "winston";
import { ethers } from "ethers";
import { Connection as SolanaConnection } from "@solana/web3.js";
import { BaseVAA } from "./utils";
import { TextDecoder } from "util";
// todo: do we need this in the plugin or just the relayer??
whSdk.setDefaultWasm("node");
function create(
commonConfig: CommonPluginEnv,
pluginConfig: any,
logger: Logger
): Plugin {
console.log("Creating da plugin...");
return new MessengerRelayerPlugin(commonConfig, pluginConfig, logger);
}
export interface MessengerRelayerPluginConfig {
registeredContracts: { chainId: whSdk.ChainId; contractAddress: string }[];
xDappConfig: XDappConfig;
messengerABI: string;
}
type Network = string;
export interface XDappConfig {
networks: Record<
Network,
{
type: string;
wormholeChainId: whSdk.ChainId;
rpc: string;
privateKey: string;
bridgeAddress: string;
}
>;
wormhole: {
restAddress: string;
};
}
// base64 encoded Buffer
type VAA = string;
export class MessengerRelayerPlugin implements Plugin<VAA> {
readonly shouldSpy: boolean = true;
readonly shouldRest: boolean = false;
readonly demoteInProgress: boolean = true;
static readonly pluginName: string = "MessengerRelayerPlugin";
readonly pluginName = MessengerRelayerPlugin.pluginName;
constructor(
readonly relayerConfig: CommonPluginEnv,
readonly pluginConfig: MessengerRelayerPluginConfig,
readonly logger: Logger
) {
this.logger.info("Messenger relayer plugin loaded");
// todo: config validation
}
async consumeEvent(
vaa: Buffer,
stagingArea: { counter?: number }
): Promise<{ workflowData?: VAA; nextStagingArea: StagingArea }> {
this.logger.info("Got VAA");
return {
workflowData: vaa.toString("base64"),
nextStagingArea: {
counter: stagingArea?.counter ? stagingArea.counter + 1 : 0,
},
};
}
async handleWorkflow(
workflow: Workflow<VAA>,
providers: Providers,
execute: ActionExecutor
): Promise<void> {
this.logger.info("Handling workflow...");
const vaa = Buffer.from(workflow.data, "base64");
const parsed = await this.parseVAA(vaa);
this.logger.info("Got message: " + Buffer.from(parsed.payload).toString("utf-8"));
await Promise.all(
this.pluginConfig.registeredContracts.map(async (registeredContract) => {
if (registeredContract.chainId === parsed.emitter_chain) {
// do not submit to emitting chain
return;
}
if (whSdk.isEVMChain(registeredContract.chainId)) {
await this.submitOnEVM(
vaa,
providers.evm[registeredContract.chainId],
execute,
registeredContract.contractAddress,
registeredContract.chainId
);
} else if (registeredContract.chainId === whSdk.CHAIN_ID_SOLANA) {
await this.submitOnSolana(
vaa,
providers.solana,
execute,
registeredContract.contractAddress,
registeredContract.chainId
);
} else {
throw new Error("Unsupported Chain: " + registeredContract.chainId);
}
})
);
this.logger.info("Message submitted to all chains!!");
}
async submitOnEVM(
vaa: Buffer,
provider: ethers.providers.Provider,
execute: ActionExecutor,
contractAddress: string,
chainId: whSdk.ChainId
) {
const messenger = new ethers.Contract(
contractAddress,
this.pluginConfig.messengerABI,
provider
);
const tx = await execute.onEVM({
chainId,
f: async ({ wallet }) => {
return messenger.connect(wallet).receiveEncodedMsg(vaa);
},
});
await tx.wait();
const message = await messenger.getCurrentMsg();
this.logger.info(`Current message now '${message}' on chain ${chainId}`);
}
async submitOnSolana(
vaa: Buffer,
provider: SolanaConnection,
execute: ActionExecutor,
contractAddress: string,
chainId: whSdk.ChainId
) {
throw new Error("Exercise for the reader...");
}
getFilters(): ContractFilter[] {
return this.pluginConfig.registeredContracts.map((rc) => ({
chainId: rc.chainId,
emitterAddress: rc.contractAddress,
}));
}
async parseVAA(vaa: Buffer | Uint8Array): Promise<BaseVAA> {
try {
const { parse_vaa } = await whSdk.importCoreWasm();
return parse_vaa(new Uint8Array(vaa)) as BaseVAA;
} catch (e) {
this.logger.error("Failed to parse vaa");
throw e;
}
}
}
const factory: PluginFactory = {
create,
pluginName: MessengerRelayerPlugin.pluginName,
};
export default factory;

View File

@ -0,0 +1,67 @@
import { readdirSync, readFileSync } from "fs";
import * as relayerEngine from "relayer-engine";
import { EnvType } from "relayer-plugin-interface";
import { MessengerRelayerPlugin, XDappConfig } from "./plugin";
async function main() {
const xDappConfig: XDappConfig = JSON.parse(
readFileSync("./xdapp.config.json").toString()
);
const messengerABI = JSON.parse(
readFileSync("./chains/evm/out/Messenger.sol/Messenger.json").toString()
).abi;
const registeredContracts = readdirSync("./deployinfo")
.filter((fname) => fname.split(".").length === 3)
.map((fname) => {
const file = JSON.parse(readFileSync(`./deployinfo/${fname}`).toString());
const network = fname.split(".")[0];
const chainId = xDappConfig.networks[network].wormholeChainId;
return { chainId, contractAddress: file.address };
});
const relayerConfig = {
envType: EnvType.LOCALHOST,
mode: relayerEngine.Mode.BOTH,
supportedChains: Object.entries(xDappConfig.networks).map(
([networkName, network]) => {
return {
chainId: network.wormholeChainId,
chainName: networkName,
nodeUrl: network.rpc,
nativeCurrencySymbol: network.wormholeChainId === 1 ? "SOL" : "ETH",
bridgeAddress: network.bridgeAddress,
};
}
),
};
const plugin = new MessengerRelayerPlugin(
relayerConfig,
{ registeredContracts, xDappConfig, messengerABI },
relayerEngine.getLogger()
);
await relayerEngine.run({
plugins: [plugin],
configs: {
executorEnv: {
// @ts-ignore
privateKeys: Object.fromEntries(
Object.values(xDappConfig.networks).map((network) => {
return [network.wormholeChainId, [network.privateKey]];
})
),
},
listenerEnv: { spyServiceHost: "localhost:7073" },
commonEnv: relayerConfig as relayerEngine.CommonEnv,
},
mode: relayerConfig.mode,
envType: relayerConfig.envType,
});
}
main().then((e) => {
console.error(e);
process.exit(1);
});

View File

@ -0,0 +1,47 @@
import * as wh from "@certusone/wormhole-sdk";
export function assertInt(x: any, fieldName?: string): number {
if (!Number.isInteger(x)) {
const e = new Error(`Expected field to be integer, found ${x}`) as any;
e.fieldName = fieldName;
throw e;
}
return x as number;
}
export function assertArray<T>(x: any, fieldName?: string): T[] {
if (!Array.isArray(x)) {
const e = new Error(`Expected field to be array, found ${x}`) as any;
e.fieldName = fieldName;
throw e;
}
return x as T[];
}
export function assertBool(x: any, fieldName?: string): boolean {
if (x !== false && x !== true) {
const e = new Error(`Expected field to be boolean, found ${x}`) as any;
e.fieldName = fieldName;
throw e;
}
return x as boolean;
}
export function nnull<T>(x: T | undefined | null, errMsg?: string): T {
if (x === undefined || x === null) {
throw new Error("Found unexpected undefined or null. " + errMsg);
}
return x;
}
export interface BaseVAA {
version: number;
guardianSetIndex: number;
timestamp: number;
nonce: number;
emitter_chain: wh.ChainId;
emitter_address: Uint8Array; // 32 bytes
sequence: number;
consistency_level: number;
payload: Uint8Array;
}

View File

@ -1,7 +1,23 @@
{
"compilerOptions": {
"types": ["node"],
"types": [
"node"
],
"moduleResolution": "node",
"esModuleInterop": true
"esModuleInterop": true,
"outDir": "lib",
"target": "esnext",
"module": "CommonJS",
"lib": [
"es2019"
],
"skipLibCheck": true,
"noImplicitAny": false,
"declaration": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"resolveJsonModule": true,
"downlevelIteration": true,
"sourceMap": true
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Run Guardiand
# Run Spy
set -euo pipefail