Skip to content

feat(SvmSpokePoolClient): relayFillStatus and fillStatusArray implementation #990

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

Merged
merged 10 commits into from
May 7, 2025

Conversation

melisaguevara
Copy link
Contributor

@melisaguevara melisaguevara commented Apr 24, 2025

This PR introduces the SVM implementation of relayFillStatus, which resolves a relay's fill status using PDA data or, if necessary, PDA events.

The resolution process is:

  1. Retrieve the fill status PDA for the relay.
  2. If querying the current slot:
    • If the PDA exists, fetch the status directly.
    • If the PDA doesn't exist but the fill deadline hasn’t passed, solve the status as Unfilled. This relies on the fact that PDAs can't be closed before the fill deadline.
  3. If the status can't be determined using the PDA, or if a specific slot is requested, fetch and sort relevant PDA events (fills and slow fill requests) to resolve it.

This PR also introduces fillStatusArray, a batched version of the same logic for resolving multiple deposits. Note that resolving status via events for large arrays is slow. If it becomes a bottleneck, we could introduce a new Unknown fill status to skip event reconstruction entirely. This should be acceptable, as event-based resolution is only needed for deposits whose fill deadline has already passed or for queries at a specific slot.

Some execution times observed during local testing:

  • Fetching data from 100 accounts takes ~250ms. Multiple requests can run concurrently.
  • Resolving statuses via events takes ~9000ms for each batch of 100 deposits.
    Note: These timings were measured without caching which could reduce them further.

Copy link

linear bot commented Apr 24, 2025

if (blockTag === "confirmed") {
toSlot = await provider.getSlot({ commitment: "confirmed" }).send();
} else {
toSlot = BigInt(blockTag);
Copy link
Contributor

@pxrl pxrl Apr 25, 2025

Choose a reason for hiding this comment

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

Is it worth validating toSlot >= fromSlot here? (nb. this assumes we retain fromSlot).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we can get rid of fromSlot.

Copy link
Contributor

@pxrl pxrl left a comment

Choose a reason for hiding this comment

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

@melisaguevara This looks great!

const relevantEvents = (
await Promise.all(
eventsToQuery.map((eventName) =>
pdaEventsClient.queryDerivedAddressEvents(eventName, BigInt(fromSlot), toSlot, { limit: 50 })
Copy link
Contributor

Choose a reason for hiding this comment

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

ooc, is there an increased cost by setting a low fromSlot here? Having to be explicit about fromSlot seems like a burden on the caller; in most (all?) cases I think the caller probably only cares about the upper bound (i.e. was this completed before some previous point in time, or alternatively, what's the current status?).

(If we do need to set reasonable bounds on fromSlot then I wonder if we could infer a safe lower bound based on the relayData.fillDeadline property)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From the events client's perspective, if we don't set fromSlot, it will fetch all events for the address. I think that should be fine for PDAs, since we expect them to have only a few events. The only case where they'd have many is if they were targeted with spam. So it seems safe to remove it, do you agree?

I've optimistically removed it here: 988c565. For now I'm only setting it as undefined when calling svmEventsClient.queryDerivedAddressEvents, but I think we can improve readability if we pass the search range as an object rather than individual params to the events client functions. I can follow up with that change after we merge the update function to avoid conflicts.

): Promise<(FillStatus | undefined)[]> {
throw new Error("fillStatusArray: not implemented");
assert(chainIsSvm(destinationChainId), "Destination chain must be an SVM chain");
const chunkSize = 2;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this intended to be 2 or 200? If I read the commit message properly then I think it hints at 200. 2 seems quite low, though we seem to lack any multicall-like read capabilities here, so going for high-parallelism adds the risk of rate-limiting as well :(

As background, the relayer can hit this function with an array of hundreds of relayData objects. If we're querying 2 at a time it's going to take quite a while to resolve that. I also wonder if we might see some nasty overheads given that relayFillStatus() instantiates a new events client for each individual query.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this was intended to be 200. I was locally testing with 2 😅

But I agree that both rate-limiting and instantiating a new events client for each query might be problematic. For the latter, I'm thinking we can use a single client instance if the pda is not a property of the class but only a param for the function that queries events. And for the former, maybe we can start with 100 parallel requests and see how that goes? Not sure there are many options for this without a multicall-like solution.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

0015fb4 This would use a single events client instance.

I'm doing some testing regarding the chunk size to have an idea of how long will it take. Will post some numbers in a bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@pxrl The performance of this is really poor. It takes up to 20 seconds to fetch 100 deposits. So I’m looking into using the fill status PDA data when possible. There’s a method that allows querying 100 accounts at once. When checking the latest slot (which is what the relayer does), we can use it to filter out deposits that have either been filled or have a slow fill requested and still have an open PDA.

For deposits that are unfilled or filled but have a closed PDA, I'm afraid we’ll still have to reconstruct from events 😞. I'm still thinking if there's any faster approach for this case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

182cde9 Based on our discussion, now we are also relying on the fill deadline to filter unfilled deposits easily and reduce the amount of statuses we have to resolve with events.

programId: Address,
relayData: RelayData,
fromSlot: number,
blockTag: number | "confirmed" = "confirmed",
Copy link
Contributor

Choose a reason for hiding this comment

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

If the delta between processed and confirmed is reliably 0 - 2 slots then I wonder if we can just revert to a raw slot number for the upper bound (i.e. toSlot?: number), where toSlot would be a caller-specified slot, or otherwise resolved dynamically to the latest confirmed slot. wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree, I covered this below.

public abstract relayFillStatus(relayData: RelayData, blockTag?: number | "latest"): Promise<FillStatus>;
public abstract relayFillStatus(
relayData: RelayData,
blockTag?: number | "latest" | "confirmed"
Copy link
Contributor

Choose a reason for hiding this comment

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

Exposing confirmed at this interface means that the caller needs to be aware of which chain type they're on, and we then need to validate in each chain-specific implementation that the correct tag was passed in. If we modify the type to be just an optional number then we could infer "chain-specific latest" if the input is undefined. That seems more platform-agnostic, though it means that we internally must decide on processed or confirmed for SVM. Based on @james-a-morris feedback about the delta being only 1 - 2 slots, it seems like we could probably live with confirmed for now. wdyt?

Suggested change
blockTag?: number | "latest" | "confirmed"
blockTag?: number

Copy link
Contributor Author

@melisaguevara melisaguevara Apr 25, 2025

Choose a reason for hiding this comment

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

I like this idea! Fwiw, we aren't relying too much on this option, the only place where I see we are using "latest" explicitly is in this line of the Dataworker. For svm it seems safe to use confirmed. So I think this makes sense, wdyt @james-a-morris?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is how it would look like d3cfa15

I wonder if we should change the name of this param too, blockTag might be confusing when used from the SVM Spoke Pool. Wdyt? It could be "atHeight" or something similar.

@melisaguevara melisaguevara force-pushed the mguevara/acx-4029-relay-fill-status branch from bd17e8d to 7dad9b1 Compare April 28, 2025 22:41
@melisaguevara melisaguevara requested a review from pxrl April 29, 2025 02:16
Copy link
Contributor

@md0x md0x left a comment

Choose a reason for hiding this comment

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

Amazing work!

Comment on lines 170 to 173
console.warn(
"fillStatusArray: Querying specific slots for large arrays is slow. " +
"For current status, omit 'atHeight' param to use latest confirmed slot instead."
);
Copy link
Member

Choose a reason for hiding this comment

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

leftover log?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, it was intentional, since reconstructing status from events is really slow I thought it would be useful to have this just in case.

Copy link
Member

Choose a reason for hiding this comment

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

Could be worth having this use a logger intstance - not sure if console goes to gcp in a nicely formatted way

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, good call! a5b03ce

Copy link
Member

@james-a-morris james-a-morris left a comment

Choose a reason for hiding this comment

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

one comment on the logger instance being used over the console

Comment on lines 170 to 173
console.warn(
"fillStatusArray: Querying specific slots for large arrays is slow. " +
"For current status, omit 'atHeight' param to use latest confirmed slot instead."
);
Copy link
Member

Choose a reason for hiding this comment

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

Could be worth having this use a logger intstance - not sure if console goes to gcp in a nicely formatted way

@melisaguevara melisaguevara merged commit eec28b8 into epic/svm-client May 7, 2025
4 checks passed
@melisaguevara melisaguevara deleted the mguevara/acx-4029-relay-fill-status branch June 18, 2025 13:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants