Skip to content

perf(svm): reuse validated Gateway vault for in-place fills - #1575

Open
Reinis-FRP wants to merge 3 commits into
reinis/svm-cu-benchmarksfrom
reinis/svm-reuse-validated-vault
Open

Reinis-FRP wants to merge 3 commits into
reinis/svm-cu-benchmarksfrom
reinis/svm-reuse-validated-vault

Conversation

@Reinis-FRP

@Reinis-FRP Reinis-FRP commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

When the committed fill recipient is GATEWAY_VAULT_AUTHORITY, reuse the writable vault already authenticated by the shared token-account loader instead of deriving, finding and parsing the same ATA again. Reuse occurs before any CPI; external recipients retain their canonical ATA and serialized-authority checks.

Restacked onto the current #1568, including #1564's self-transfer implementation. All fills still execute SPL transfer_checked; balance, frozen-state and delegate/allowance checks remain in that CPI. The loader tests cover both token programs and reject missing, readonly, malformed, wrong-program, wrong-mint and wrong-authority vaults. Public IDLs and account ordering are unchanged. Fixes #1570.

Measured execution CU medians against the same updated base:

Flow Before After Delta
v5-deposit 73,516 73,516 +0
v5-external-fill 84,564 84,582 +18
v5-inplace-fill 91,841 89,793 -2,048

Self-transfer savings range from 2,048 to 9,548 CU per fixture, with a median per-fixture saving of 3,548 CU. The difference between execution medians is 2,048 CU; differences of medians need not equal the median of differences. External fills cost 18 CU more in every fixture. Legacy flows and per-fixture approval/buffer costs are unchanged. These are consumed-CU measurements on the documented local runtime, not production budgets.

Validation after restacking: 18 Rust tests, all 25 CU fixtures, and all 14 fill validator tests passed. Coverage includes insufficient balance/allowance, frozen accounts, wrong account authority, PermanentDelegate rejection, rollback and rent reclaim. Guarded SBF builds passed; the validator-test Spoke binary matches the measured binary byte for byte. Formatting and diff checks passed. Full verified-build results are tracked separately in CI.

Targets #1568 (reinis/svm-cu-benchmarks), independently of #1573.

Per-fixture execution CU and provenance

Before snapshot and after snapshot use Node 22.22.0, Agave 4.1.2, cargo-build-sbf 4.1.0 and platform-tools 1.44/1.52/1.54 for legacy/Spoke/Gateway. Fixture, runtime, public IDL, legacy and Gateway hashes match. Approval and buffer costs are unchanged per fixture. Source revisions, tracked source-diff hashes and binary hashes are recorded in the snapshots.

Flow Fixture Before After Delta
legacy-deposit 0 42,207 42,207 +0
legacy-fill 0 41,021 41,021 +0
legacy-deposit 1 34,707 34,707 +0
legacy-fill 1 44,021 44,021 +0
legacy-deposit 2 34,707 34,707 +0
legacy-fill 2 39,521 39,521 +0
legacy-deposit 3 34,707 34,707 +0
legacy-fill 3 39,521 39,521 +0
legacy-deposit 4 34,707 34,707 +0
legacy-fill 4 51,521 51,521 +0
v5-deposit 0 79,516 79,516 +0
v5-external-fill 0 84,564 84,582 +18
v5-inplace-fill 0 97,841 92,793 -5,048
v5-deposit 1 73,516 73,516 +0
v5-external-fill 1 80,064 80,082 +18
v5-inplace-fill 1 91,841 89,793 -2,048
v5-deposit 2 73,516 73,516 +0
v5-external-fill 2 83,064 83,082 +18
v5-inplace-fill 2 91,841 88,293 -3,548
v5-deposit 3 82,516 82,516 +0
v5-external-fill 3 93,564 93,582 +18
v5-inplace-fill 3 111,341 101,793 -9,548
v5-deposit 4 67,516 67,516 +0
v5-external-fill 4 86,064 86,082 +18
v5-inplace-fill 4 87,341 85,293 -2,048

Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-02T11:47:59.243919Z 0e5a1cc PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Reinis-FRP
Reinis-FRP force-pushed the reinis/svm-reuse-validated-vault branch from 0e5a1cc to 41f1ccc Compare October 2, 2026 11:47
@Reinis-FRP
Reinis-FRP requested a review from droplet-rl October 2, 2026 11:48

@droplet-rl droplet-rl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The change is correct, and the security properties are the same as before. Approving. My comments below are non-blocking.

Why the reuse is safe

  • If fill_input.recipient == GATEWAY_VAULT_AUTHORITY, the old code derived ATA(GATEWAY_VAULT_AUTHORITY, output_token, token_program.key). That is the same address V5TokenAccounts::load derives: token_program.key is token_program_id and mint is output_token.
  • Both paths called find_v5_account with the same key, so they got the same first-match AccountInfo. That is true even if remaining_accounts has duplicate keys.
  • The writable check and load_token_account(.., mint, GATEWAY_VAULT_AUTHORITY) had already run on that account, with no CPI in between. The second lookup and parse added nothing.
  • On the external path, recipient != GATEWAY_VAULT_AUTHORITY. The ATA seeds are fixed-length (owner‖token_program‖mint), so its ATA can only match the vault through a PDA collision. Choosing delivery by recipient therefore gives the same result as the old key compare.
  • The live balance check, the delegate check, and relay.recipient == fill_input.recipient in the handler are unchanged.

What I checked

  • Ran cargo test -p svm-spoke --lib v5_adapter::fill locally: both new tests pass.
  • External-recipient rejection cases (wrong recipient account, mint mismatch, reassigned ATA authority, missing approval) are still covered by test/svm/SvmSpoke.V5Fill.ts.
  • Recomputed the README normalization from baseline.json. The medians (84,688 / 73,837), normalized execution (78,688 / 69,337), the 9,351 normalized gap on every sample, sample 3's 6,351, and the 15,351 total-median gap all match.

Non-blocking

  1. The +28 CU on external fills could probably be removed. delivery is now decided by a second key compare that always matches on the in-place path and can never match on the external path. Choosing delivery inside the same if/else drops that compare from both paths (inline suggestion). I checked that it compiles and that both new tests pass; I haven't re-run the CU benchmark.
  2. Two small README wording points (inline).
  3. Optional: baseline.json records head: 24e07c9c plus a non-empty trackedDiffSha256, so the snapshot points at the parent commit plus an uncommitted diff. Your binary-hash check (80510beb…) shows the numbers are identical. Still, if you regenerate the snapshot from a clean tree at the final commit, the file explains itself without the PR description.

Comment on lines +147 to +166
let recipient_info = if fill_input.recipient == GATEWAY_VAULT_AUTHORITY {
// The shared loader validated this writable vault, including its token authority. No CPI has intervened.
token_accounts.gateway_vault.clone()
} else {
let recipient = get_associated_token_address_with_program_id(
&fill_input.recipient,
&fill_input.output_token,
token_accounts.token_program.key,
);
let recipient_info = find_v5_account(remaining_accounts, &recipient, true)?;
load_token_account(
recipient_info,
token_accounts.token_program.key,
&fill_input.output_token,
&fill_input.recipient,
)?;
recipient_info.clone()
};

let delivery = if token_accounts.gateway_vault.key() == recipient_info.key() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit / possible CU win: once the branch is on fill_input.recipient, gateway_vault.key() == recipient_info.key() on line 166 always matches on the in-place path and can only match on the external path through an ATA PDA collision. Deciding delivery inside the same branch removes one 32-byte compare from both paths, which may recover most of the +28 CU on external fills. Error order on both paths stays the same (recipient checks, then delegate; or the balance check):

let (recipient_info, delivery) = if fill_input.recipient == GATEWAY_VAULT_AUTHORITY {
    // The shared loader validated this writable vault, including its token authority. No CPI has intervened.
    // This check is not a debit. Builders must consume after one fill or enforce an aggregate floor covering
    // every in-place fill recorded before full-balance consumption; step-root reuse alone is valid.
    require!(token_accounts.source.amount >= output_amount, V5Error::InsufficientVaultBalance);
    (token_accounts.gateway_vault.clone(), V5FillDelivery::InPlace)
} else {
    let recipient = get_associated_token_address_with_program_id(
        &fill_input.recipient,
        &fill_input.output_token,
        token_accounts.token_program.key,
    );
    let recipient_info = find_v5_account(remaining_accounts, &recipient, true)?;
    load_token_account(
        recipient_info,
        token_accounts.token_program.key,
        &fill_input.output_token,
        &fill_input.recipient,
    )?;
    require!(token_accounts.source.delegate == COption::Some(V5_FILL_DELEGATE), V5Error::InvalidTokenAccount);
    let delegate = find_v5_account(remaining_accounts, &V5_FILL_DELEGATE, false)?.clone();
    (recipient_info.clone(), V5FillDelivery::Delegated(delegate))
};

I checked that this compiles and that both new loader tests pass. I haven't re-run the CU benchmark, so the actual saving still needs measuring. If you'd rather keep the explicit key compare as defense-in-depth, that's fine too. In that case, a short comment saying it can't change the outcome would help.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed that the comparison is redundant. We’ll leave this cleanup out because #1574 removes V5FillDelivery and the comparison entirely, using one transfer path for both destinations. The validated-vault reuse in this PR still applies there; we’ll carry that over and refresh the combined CU measurements when integrating the two. No need to preserve the comparison as defense in depth.


Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖

Comment thread test/svm-gateway/README.md Outdated
Comment on lines +97 to +98
External minus in-place execution is 9,351 CU after normalization for every sample. Both fill variants have the same
number of Gateway-vault ATA derivations. Sample 3's in-place status bump is two steps lower (3,000 extra CU), leaving

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Small wording point: the old paragraph singled out sample 3 because it was a reversal (in-place cost more than external). After this change sample 3 is no longer a reversal, and sample 4 now differs from the normalized gap by a similar amount in the other direction: measured gap 13,851, because the external status bump is 250 and the in-place one is 253, so +4,500. Either explain both samples, or replace the sample-3 sentence with a general one ("per-sample gaps differ only by status-bump differences").

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated in 7b0bf59: replaced the sample-3 example with the general explanation that per-sample execution gaps differ from the normalized gap only by status-bump differences. Checked the formula against all five fixtures, including sample 4’s 13,851 CU gap.


Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖

Comment thread test/svm-gateway/README.md Outdated
slots and temporary paths in raw receipts naturally vary. Before committing an updated snapshot, run
`yarn prettier --write test/svm-gateway/cu/baseline.json`.

Validated Gateway vault reuse is tracked in [#1570](https://github.com/across-protocol/contracts/issues/1570).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This PR closes #1570, so "is tracked in" will read as still pending after merge. Maybe "Validated Gateway vault reuse for in-place fills landed in #1575 (#1570)", or drop the line since the PR and snapshot links already record where it came from.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed the tracking sentence in 7b0bf59. The README keeps the current baseline and interpretation; the implementation history stays in this PR.


Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖

@droplet-rl

Copy link
Copy Markdown
Contributor

🔎 View trace

@Reinis-FRP
Reinis-FRP force-pushed the reinis/svm-reuse-validated-vault branch from 7b0bf59 to 70d4348 Compare October 5, 2026 07:48
@Reinis-FRP
Reinis-FRP force-pushed the reinis/svm-cu-benchmarks branch 2 times, most recently from 70888c2 to 3d3178f Compare October 5, 2026 11:27
@Reinis-FRP
Reinis-FRP force-pushed the reinis/svm-reuse-validated-vault branch from 70d4348 to 1efdc02 Compare October 5, 2026 11:27
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
@Reinis-FRP
Reinis-FRP force-pushed the reinis/svm-cu-benchmarks branch from 3d3178f to a537c44 Compare October 5, 2026 12:04
@Reinis-FRP
Reinis-FRP force-pushed the reinis/svm-reuse-validated-vault branch from 1efdc02 to 788437c Compare October 5, 2026 12:04
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.

2 participants