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

watcher: solana shim support #421

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions watcher/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"algosdk": "^2.4.0",
"anchor-0.29.0": "npm:@coral-xyz/anchor@^0.29.0",
"aptos": "^1.4.0",
"binary-layout": "^1.2.0",
"bs58": "^5.0.0",
"dotenv": "^16.0.3",
"firebase-admin": "^11.4.0",
Expand Down
99 changes: 88 additions & 11 deletions watcher/src/watchers/SolanaWatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,37 @@ import {
universalAddress_stripped,
} from '@wormhole-foundation/wormhole-monitor-common';
import { Watcher } from './Watcher';
import { Network, contracts } from '@wormhole-foundation/sdk-base';
import { Network, contracts, encoding } from '@wormhole-foundation/sdk-base';
import { deserializePostMessage } from '@wormhole-foundation/sdk-solana-core';
import { getAllKeys } from '../utils/solana';
import { UniversalAddress } from '@wormhole-foundation/sdk-definitions';
import { DeriveType, deserialize, Layout } from 'binary-layout';

const COMMITMENT: Commitment = 'finalized';
const GET_SIGNATURES_LIMIT = 1000;

const ShimContracts: { [key in Network]: string } = {
Mainnet: '',
Testnet: 'EtZMZM22ViKMo4r5y4Anovs3wKQ2owUmDpjygnMMcdEX',
Devnet: '',
};

const POST_MESSAGE_INSTRUCTION_ID = 0x01;
const POST_MESSAGE_UNRELIABLE_INSTRUCTION_ID = 0x08;
const shimMessageEventDiscriminator = 'e445a52e51cb9a1d441b8f004d4c8970';

const shimMessageEventLayout = [
{ name: 'discriminator', binary: 'bytes', size: 16 },
{ name: 'emitterAddress', binary: 'bytes', size: 32 },
{ name: 'sequence', binary: 'uint', size: 8, endianness: 'little' },
{ name: 'timestamp', binary: 'uint', size: 4, endianness: 'little' },
] as const satisfies Layout;
export type ShimMessageEvent = DeriveType<typeof shimMessageEventLayout>;

export class SolanaWatcher extends Watcher {
readonly rpc: string;
readonly programId: string;
readonly shimProgramId: string;
// this is set as a class field so we can modify it in tests
getSignaturesLimit = GET_SIGNATURES_LIMIT;
// The Solana watcher uses the `getSignaturesForAddress` RPC endpoint to fetch all transactions
Expand All @@ -43,6 +64,7 @@ export class SolanaWatcher extends Watcher {
super(network, 'Solana', mode);
this.rpc = RPCS_BY_CHAIN[this.network].Solana!;
this.programId = contracts.coreBridge(this.network, 'Solana');
this.shimProgramId = ShimContracts[this.network];
}

getConnection(): Connection {
Expand Down Expand Up @@ -162,39 +184,75 @@ export class SolanaWatcher extends Watcher {

const accountKeys = await getAllKeys(this.getConnection(), res);
const programIdIndex = accountKeys.findIndex((i) => i.toBase58() === this.programId);
const shimProgramIdIndex = accountKeys.findIndex(
(i) => i.toBase58() === this.shimProgramId
);
const message: VersionedMessage = res.transaction.message;
const instructions = message.compiledInstructions;
const innerInstructions =
res.meta?.innerInstructions?.flatMap((i) =>
i.instructions.map(normalizeCompileInstruction)
) || [];

// Need to look for Wormhole instructions and shim instructions
const whInstructions = innerInstructions
.concat(instructions)
.filter((i) => i.programIdIndex === programIdIndex);
.filter(
(i) => i.programIdIndex === programIdIndex || i.programIdIndex === shimProgramIdIndex
);

const blockKey = makeBlockKey(
res.slot.toString(),
new Date(res.blockTime * 1000).toISOString()
);

let needShim = false;
let gotShim = false;
const vaaKeys: string[] = [];
for (const instruction of whInstructions) {
// skip if not postMessage instruction
const instructionId = instruction.data;
if (instructionId[0] !== 0x08 && instructionId[0] !== 0x01) continue;
if (
instruction.programIdIndex === programIdIndex &&
instructionId[0] === POST_MESSAGE_UNRELIABLE_INSTRUCTION_ID
) {
// TODO: Do I need to verify that this message has no data?
// This is an unreliable wormhole message. It is only used in conjunction with a shim message.
needShim = true;
continue;
}

const accountId = accountKeys[instruction.accountKeyIndexes[1]];
let emitterAddress: UniversalAddress;
let sequence: bigint;

const acctInfo = await this.getConnection().getAccountInfo(accountId, COMMITMENT);
if (!acctInfo?.data) throw new Error('No data found in message account');
const { emitterAddress, sequence } = deserializePostMessage(
new Uint8Array(acctInfo.data)
);
if (instruction.programIdIndex === programIdIndex) {
if (instructionId[0] !== POST_MESSAGE_INSTRUCTION_ID) {
console.log('Got non-post message instruction');
continue;
}
const accountId = accountKeys[instruction.accountKeyIndexes[1]];

const acctInfo = await this.getConnection().getAccountInfo(accountId, COMMITMENT);
if (!acctInfo?.data) throw new Error('No data found in message account');
const deserializedMsg = deserializePostMessage(new Uint8Array(acctInfo.data));
emitterAddress = deserializedMsg.emitterAddress;
sequence = deserializedMsg.sequence;
} else {
// instruction.programIdIndex === shimProgramIdIndex
console.log('Got shim instruction');
gotShim = true;
const parsedMsg = this.parseShimMessage(instruction.data);
if (!parsedMsg) {
console.log('Failed to parse shim message');
continue;
}
emitterAddress = parsedMsg.emitterAddress;
sequence = parsedMsg.sequence;
}
// TODO: should I check if needShim === gotShim?

vaaKeys.push(
makeVaaKey(
res.transaction.signatures[0],
res.transaction.signatures[0], // This is the tx hash
this.chain,
universalAddress_stripped(emitterAddress),
sequence.toString()
Expand All @@ -217,6 +275,25 @@ export class SolanaWatcher extends Watcher {
return { vaasByBlock: { [lastBlockKey]: [], ...vaasByBlock } };
}

parseShimMessage(data: Uint8Array): {
emitterAddress: UniversalAddress;
sequence: bigint;
} | null {
// First step is to convert the data into a hex string
const hexData = encoding.hex.encode(data);

// Next, we need to look for the discriminator that we care about.
if (hexData.startsWith(shimMessageEventDiscriminator)) {
// Use the binary layout to deserialize the data
const decoded = deserialize(shimMessageEventLayout, data);
const emitterAddress = new UniversalAddress(decoded.emitterAddress);
const sequence = BigInt(decoded.sequence);

return { emitterAddress, sequence };
}
return null;
}

isValidVaaKey(key: string) {
try {
const [txHash, vaaKey] = key.split(':');
Expand Down
Loading