Skip to content

feat: create svm spoke _update #976

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

Open
wants to merge 16 commits into
base: epic/svm-client
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions src/arch/svm/eventsClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,8 @@ export class SvmSpokeEventsClient {

return events;
}

public getSvmSpokeAddress(): Address {
return this.svmSpokeAddress;
}
}
11 changes: 9 additions & 2 deletions src/clients/BaseAbstractClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { providers } from "ethers";
import { CachingMechanismInterface } from "../interfaces";
import { EventSearchConfig, isDefined, MakeOptional } from "../utils";
import { Rpc, RpcTransport, SolanaRpcApiFromTransport } from "@solana/kit";

export enum UpdateFailureReason {
NotReady,
Expand Down Expand Up @@ -58,15 +59,21 @@ export abstract class BaseAbstractClient {
* @provider Ethers RPC provider instance.
* @returns An EventSearchConfig instance if valid, otherwise an UpdateFailureReason.
*/
public async updateSearchConfig(provider: providers.Provider): Promise<EventSearchConfig | UpdateFailureReason> {
public async updateSearchConfig(
provider: providers.Provider | Rpc<SolanaRpcApiFromTransport<RpcTransport>>
): Promise<EventSearchConfig | UpdateFailureReason> {
const fromBlock = this.firstBlockToSearch;
let { toBlock } = this.eventSearchConfig;
if (isDefined(toBlock)) {
if (fromBlock > toBlock) {
throw new Error(`Invalid event search config fromBlock (${fromBlock}) > toBlock (${toBlock})`);
}
} else {
toBlock = await provider.getBlockNumber();
if (provider instanceof providers.Provider) {
toBlock = await provider.getBlockNumber();
} else {
toBlock = Number(await provider.getBlockHeight({ commitment: "confirmed" }).send());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One observation: here we're querying the latest block and then down in eventsClient that is translated to a slot number via binary search. I think we could short-circuit all this by querying provider.getSlot({ commitment: "confirmed" }). Do you think the eventsClient could accept something akin to an EVM BlockTag, such that confirmed results in a call to provider.getSlot() to resolve at the specified commitment, and a block number would result in a call to getSlotForBlock() ?

(nb. we could also generalise EventSearchConfig by renaming fromBlock -> from and toBlock -> to)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair - my only worry about something like this is that we're now moving away from the "blocks" model upstream

}
if (toBlock < fromBlock) {
return UpdateFailureReason.AlreadyUpdated;
}
Expand Down
7 changes: 6 additions & 1 deletion src/clients/SpokePoolClient/EVMSpokePoolClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,16 @@ export class EVMSpokePoolClient extends SpokePoolClient {
// Sort all events to ensure they are stored in a consistent order.
events.forEach((events) => sortEventsAscendingInPlace(events));

// Map events to SortableEvent
const eventsWithBlockNumber = events.map((eventList) =>
eventList.map((event) => spreadEventWithBlockNumber(event))
);

return {
success: true,
currentTime: currentTime.toNumber(), // uint32
searchEndBlock: searchConfig.toBlock,
events,
events: eventsWithBlockNumber,
};
}

Expand Down
184 changes: 93 additions & 91 deletions src/clients/SpokePoolClient/SVMSpokePoolClient.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { Rpc, RpcTransport, SolanaRpcApiFromTransport } from "@solana/kit";
import winston from "winston";
import { Rpc, SolanaRpcApiFromTransport, RpcTransport } from "@solana/kit";
import { BigNumber, DepositSearchResult, EventSearchConfig, MakeOptional } from "../../utils";
import { SvmSpokeEventsClient, SVMEventNames } from "../../arch/svm";
import { SVMEventNames, SvmSpokeEventsClient } from "../../arch/svm";
import { FillStatus, RelayData, SortableEvent } from "../../interfaces";
import {
BigNumber,
DepositSearchResult,
EventSearchConfig,
MakeOptional,
sortEventsAscendingInPlace,
toBN,
} from "../../utils";
import { isUpdateFailureReason } from "../BaseAbstractClient";
import { HubPoolClient } from "../HubPoolClient";
import { knownEventNames, SpokePoolClient, SpokePoolUpdate } from "./SpokePoolClient";
import { RelayData, FillStatus } from "../../interfaces";

/**
* SvmSpokePoolClient is a client for the SVM SpokePool program. It extends the base SpokePoolClient
* and implements the abstract methods required for interacting with an SVM Spoke Pool.
Expand Down Expand Up @@ -60,93 +69,86 @@ export class SvmSpokePoolClient extends SpokePoolClient {
/**
* Performs an update to refresh the state of this client by querying SVM events.
*/
protected _update(_eventsToQuery: string[]): Promise<SpokePoolUpdate> {
throw new Error("update not implemented for SVM");
// const searchStartSlot = BigInt(this.firstBlockToSearch);
// let searchEndSlot: bigint;
// try {
// // Determine the end slot for the search
// if (this.eventSearchConfig.toBlock !== undefined) {
// searchEndSlot = BigInt(this.eventSearchConfig.toBlock);
// } else {
// const latestSlot = await this.rpc.getSlot({ commitment: "confirmed" }).send();
// // Use default 0 for maxBlockLookBack if not provided
// const lookBackBy = BigInt(this.eventSearchConfig.maxBlockLookBack ?? 0);
// const lookBackLimitSlot = lookBackBy > 0 ? latestSlot - lookBackBy : BigInt(0);
// const effectiveStartSlot = searchStartSlot > lookBackLimitSlot ? searchStartSlot : lookBackLimitSlot;
// // Ensure end slot is not before start slot
// searchEndSlot = latestSlot > effectiveStartSlot ? latestSlot : effectiveStartSlot;
// if (effectiveStartSlot > searchEndSlot) {
// this.log("info", `Start slot ${effectiveStartSlot} is after end slot ${searchEndSlot}, nothing to query.`);
// return {
// success: true,
// currentTime: this.currentTime, // No time update if no query
// events: [],
// // Report the block *before* the effective start if nothing was queried
// searchEndBlock: Number(effectiveStartSlot) - 1,
// };
// }
// }
// this.log("debug", `Querying SVM events from slot ${searchStartSlot} to ${searchEndSlot}`);
// // Query events for each requested type using the public method
// const allQueriedEvents: Log[] = [];
// for (const eventName of eventsToQuery) {
// // Cast string eventName to the specific EventName type
// const typedEventName = eventName as EventName;
// const events = await this.svmEventsClient.queryEvents<EventData>(
// typedEventName,
// searchStartSlot,
// searchEndSlot,
// {
// commitment: "confirmed",
// }
// );
// // Map SVM event structure to expected Log structure
// const mappedEvents: Log[] = events.map((event) => ({
// name: event.name,
// args: event.data as any, // eslint-disable-line @typescript-eslint/no-explicit-any
// blockNumber: Number(event.slot), // Using slot as blockNumber (potential precision loss for very large slots)
// transactionHash: event.signature.toString(),
// logIndex: 0, // SVM doesn't have a direct logIndex equivalent per event in a tx? Assign 0
// address: event.program.toString(), // Program address
// blockHash: event.blockHash.toString(),
// transactionIndex: event.transactionIndex,
// }));
// allQueriedEvents.push(...mappedEvents);
// }
// // Group events by name
// const groupedEvents: { [eventName: string]: Log[] } = {};
// for (const event of allQueriedEvents) {
// groupedEvents[event.name] = groupedEvents[event.name] || [];
// groupedEvents[event.name].push(event);
// }
// // Sort events within each group by blockNumber (slot) (ascending)
// // and prepare final results array in the order requested by eventsToQuery
// const queryResults: Log[][] = eventsToQuery.map((eventName) => {
// const events = groupedEvents[eventName] || [];
// events.sort((a, b) => a.blockNumber - b.blockNumber);
// return events;
// });
// // TODO: Implement processing logic similar to the EVM version in SpokePoolClient.update
// // This involves taking the `queryResults` and updating internal state like
// // this.depositHashes, this.fills, this.speedUps, etc., based on the event data.
// // This current implementation only fetches the events but doesn't process them into state.
// // Placeholder for current time - get timestamp of the searchEndSlot
// // Handle case where searchEndSlot might be 0 or negative if calculation results in it.
// const currentTime = searchEndSlot > 0 ? await this.getTimestampForBlock(Number(searchEndSlot)) : 0;
// return {
// success: true,
// currentTime: currentTime,
// events: queryResults, // Pass the structured events
// searchEndBlock: Number(searchEndSlot), // Use slot number
// };
// } catch (error: unknown) {
// this.log("error", "Failed to update SVM SpokePoolClient during event fetching or processing", {
// error: error instanceof Error ? error.message : String(error),
// });
// // Use correct enum casing
// return { success: false, reason: UpdateFailureReason.BadRequest };
// }
protected async _update(eventsToQuery: string[]): Promise<SpokePoolUpdate> {
const searchConfig = await this.updateSearchConfig(this.rpc);
if (isUpdateFailureReason(searchConfig)) {
const reason = searchConfig;
return { success: false, reason };
}

const eventSearchConfigs = eventsToQuery.map((eventName) => {
if (!this._queryableEventNames().includes(eventName)) {
throw new Error(`SpokePoolClient: Cannot query unrecognised SpokePool event name: ${eventName}`);
}

const _searchConfig = { ...searchConfig }; // shallow copy

// By default, an event's query range is controlled by the `eventSearchConfig` passed in during instantiation.
// However, certain events have special overriding requirements to their search ranges:
// - EnabledDepositRoute: The full history is always required, so override the requested fromBlock.
if (eventName === "EnabledDepositRoute" && !this.isUpdated) {
_searchConfig.fromBlock = this.deploymentBlock;
}

return _searchConfig as EventSearchConfig;
});

const spokePoolAddress = this.svmEventsClient.getSvmSpokeAddress();

this.log("debug", `Updating SpokePool client for chain ${this.chainId}`, {
eventsToQuery,
searchConfig,
spokePool: spokePoolAddress,
});

const timerStart = Date.now();

const [currentTime, ...eventsQueried] = await Promise.all([
this.rpc.getBlockTime(BigInt(searchConfig.toBlock)).send(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RE slots and blocks, there's still nuance here because toBlock is a block, but getBlockTime expects a slot.

I'm wondering if the SpokePoolClient needs to have responsibility for mapping blocks to slots internally. I'm hacking on an example of how we might do this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow that's super strange - the function takes a name called blockNumber but the full definition is below. Very good catch

import type { Slot, UnixTimestamp } from '@solana/rpc-types';
/** Estimated production time, as Unix timestamp (seconds since the Unix epoch) */
type GetBlockTimeApiResponse = UnixTimestamp;
export type GetBlockTimeApi = {
    /**
     * Returns the estimated production time of a block.
     */
    getBlockTime(
    /** block number, identified by Slot */
    blockNumber: Slot): GetBlockTimeApiResponse;
};

...eventsToQuery.map(async (eventName, idx) => {
const config = eventSearchConfigs[idx];
const events = await this.svmEventsClient.queryEvents(
eventName as SVMEventNames,
BigInt(config.fromBlock),
BigInt(config.toBlock)
);
return Promise.all(
events.map(async (event): Promise<SortableEvent> => {
const block = await this.rpc.getBlock(event.slot).send();

if (!block) {
this.log("error", `SpokePoolClient::update: Failed to get block for slot ${event.slot}`);
throw new Error(`SpokePoolClient::update: Failed to get block for slot ${event.slot}`);
}

return {
transactionHash: event.signature.toString(),
blockNumber: Number(block.blockHeight),
transactionIndex: 0,
logIndex: 0,
...event.data,
};
})
);
}),
]);
this.log("debug", `Time to query new events from RPC for ${this.chainId}: ${Date.now() - timerStart} ms`);
if (!BigNumber.isBigNumber(currentTime) || currentTime.lt(this.currentTime)) {
const errMsg = BigNumber.isBigNumber(currentTime)
? `currentTime: ${currentTime} < ${toBN(this.currentTime)}`
: `currentTime is not a BigNumber: ${JSON.stringify(currentTime)}`;
throw new Error(`SpokePoolClient::update: ${errMsg}`);
}

// Sort all events to ensure they are stored in a consistent order.
eventsQueried.forEach((events) => sortEventsAscendingInPlace(events));

return {
success: true,
currentTime: currentTime.toNumber(), // uint32
searchEndBlock: searchConfig.toBlock,
events: eventsQueried,
};
}

/**
Expand Down
42 changes: 20 additions & 22 deletions src/clients/SpokePoolClient/SpokePoolClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,20 @@ import {
toAddress,
validateFillForDeposit,
} from "../../utils";
import {
duplicateEvent,
sortEventsAscendingInPlace,
spreadEvent,
spreadEventWithBlockNumber,
} from "../../utils/EventUtils";
import { duplicateEvent, sortEventsAscendingInPlace } from "../../utils/EventUtils";
import { ZERO_ADDRESS } from "../../constants";
import {
Deposit,
DepositWithBlock,
EnabledDepositRouteWithBlock,
Fill,
FillStatus,
FillWithBlock,
Log,
RelayData,
RelayerRefundExecutionWithBlock,
RootBundleRelayWithBlock,
SlowFillRequestWithBlock,
SortableEvent,
SpeedUpWithBlock,
TokensBridged,
} from "../../interfaces";
Expand All @@ -47,7 +43,7 @@ import { HubPoolClient } from "../HubPoolClient";
export type SpokePoolUpdateSuccess = {
success: true;
currentTime: number;
events: Log[][];
events: SortableEvent[][];
searchEndBlock: number;
};
export type SpokePoolUpdateFailure = {
Expand Down Expand Up @@ -459,7 +455,7 @@ export abstract class SpokePoolClient extends BaseAbstractClient {
* @see _update
*/
public async update(eventsToQuery = this._queryableEventNames()): Promise<void> {
const duplicateEvents: Log[] = [];
const duplicateEvents: SortableEvent[] = [];
if (this.hubPoolClient !== null && !this.hubPoolClient.isUpdated) {
throw new Error("HubPoolClient not updated");
}
Expand All @@ -472,7 +468,7 @@ export abstract class SpokePoolClient extends BaseAbstractClient {

if (eventsToQuery.includes("TokensBridged")) {
for (const event of queryResults[eventsToQuery.indexOf("TokensBridged")]) {
this.tokensBridged.push(spreadEventWithBlockNumber(event) as TokensBridged);
this.tokensBridged.push(event as TokensBridged);
}
}

Expand All @@ -492,15 +488,15 @@ export abstract class SpokePoolClient extends BaseAbstractClient {
// For each deposit, resolve its quoteTimestamp to a block number on the HubPool.
// Don't bother filtering for uniqueness; the HubPoolClient handles this efficienctly.
const quoteBlockNumbers = await this.getBlockNumbers(
depositEvents.map(({ args }) => Number(args["quoteTimestamp"]))
depositEvents.map((e) => (e as DepositWithBlock).quoteTimestamp)
);
for (const event of depositEvents) {
const quoteBlockNumber = quoteBlockNumbers[Number(event.args["quoteTimestamp"])];
const quoteBlockNumber = quoteBlockNumbers[Number((event as DepositWithBlock).quoteTimestamp)];

// Derive and append the common properties that are not part of the onchain event.
const deposit = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@james-a-morris Can we also now drop the as DepositWithBlock type assertion down on line 503 below?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

...spreadEventWithBlockNumber(event),
messageHash: getMessageHash(event.args.message),
...event,
messageHash: getMessageHash((event as DepositWithBlock).message),
quoteBlockNumber,
originChainId: this.chainId,
// The following properties are placeholders to be updated immediately.
Expand Down Expand Up @@ -540,7 +536,7 @@ export abstract class SpokePoolClient extends BaseAbstractClient {
const speedUpEvents = queryResults[eventsToQuery.indexOf(eventName)] ?? [];

for (const event of speedUpEvents) {
const speedUp = { ...spreadEventWithBlockNumber(event), originChainId: this.chainId } as SpeedUpWithBlock;
const speedUp = { ...event, originChainId: this.chainId } as SpeedUpWithBlock;
assign(this.speedUps, [speedUp.depositor, speedUp.depositId.toString()], [speedUp]);

// Find deposit hash matching this speed up event and update the deposit data associated with the hash,
Expand Down Expand Up @@ -568,7 +564,7 @@ export abstract class SpokePoolClient extends BaseAbstractClient {
const slowFillRequests = queryResults[eventsToQuery.indexOf(eventName)];
for (const event of slowFillRequests) {
const slowFillRequest = {
...spreadEventWithBlockNumber(event),
...event,
destinationChainId: this.chainId,
} as SlowFillRequestWithBlock;

Expand Down Expand Up @@ -608,13 +604,15 @@ export abstract class SpokePoolClient extends BaseAbstractClient {
// test that the types are complete. A broader change in strategy for safely unpacking events will be introduced.
for (const event of fillEvents) {
const fill = {
...spreadEventWithBlockNumber(event),
...event,
destinationChainId: this.chainId,
} as FillWithBlock;

if (eventName === "FilledV3Relay") {
fill.messageHash = getMessageHash(event.args.message);
fill.relayExecutionInfo.updatedMessageHash = getMessageHash(event.args.relayExecutionInfo.updatedMessage);
fill.messageHash = getMessageHash((event as unknown as { message: string }).message);
fill.relayExecutionInfo.updatedMessageHash = getMessageHash(
(event as FillWithBlock).relayExecutionInfo.updatedMessage!
);
}

// Sanity check that this event is not a duplicate.
Expand All @@ -640,7 +638,7 @@ export abstract class SpokePoolClient extends BaseAbstractClient {
const enableDepositsEvents = queryResults[eventsToQuery.indexOf("EnabledDepositRoute")];

for (const event of enableDepositsEvents) {
const enableDeposit = spreadEvent(event.args);
const enableDeposit = event as EnabledDepositRouteWithBlock;
assign(
this.depositRoutes,
[enableDeposit.originToken, enableDeposit.destinationChainId],
Expand All @@ -652,14 +650,14 @@ export abstract class SpokePoolClient extends BaseAbstractClient {
if (eventsToQuery.includes("RelayedRootBundle")) {
const relayedRootBundleEvents = queryResults[eventsToQuery.indexOf("RelayedRootBundle")];
for (const event of relayedRootBundleEvents) {
this.rootBundleRelays.push(spreadEventWithBlockNumber(event) as RootBundleRelayWithBlock);
this.rootBundleRelays.push(event as RootBundleRelayWithBlock);
}
}

if (eventsToQuery.includes("ExecutedRelayerRefundRoot")) {
const refundEvents = queryResults[eventsToQuery.indexOf("ExecutedRelayerRefundRoot")];
for (const event of refundEvents) {
const executedRefund = spreadEventWithBlockNumber(event) as RelayerRefundExecutionWithBlock;
const executedRefund = event as RelayerRefundExecutionWithBlock;
this.relayerRefundExecutions.push(executedRefund);
}
}
Expand Down
Loading