Repository navigation
feat(svm): retire slow fills (ACP-221) - #1550
Conversation
droplet-rl
left a comment
There was a problem hiding this comment.
Reviewed the diff against reinis/acp-184-step-5-gateway-integration. The removal is clean and the compatibility argument holds up — I re-derived most of the constants rather than taking them on trust, and everything checked out. No code defects found. One deployment-sequencing question I'd like answered before this ships, plus a few nits.
What I independently verified
- Retired selectors.
[39,157,165,187,88,217,207,98]and[26,207,3,168,193,252,59,127]are exactlysha256("global:request_slow_fill")[..8]andsha256("global:execute_slow_relay_leaf")[..8]. The program declares no#[fallback], so both genuinely land on Anchor's default dispatch arm →InstructionFallbackNotFound(101), before any account validation. - Frozen account fixture.
legacy_requested_slow_fill.jsondecodes to 45 bytes: discriminatorsha256("account:FillStatusAccount")[..8],status = 1,fill_deadline = 4000000000.owneris the declared program ID, andlamports = 1204080is exactly the rent-exempt minimum for 45 bytes ((128+45) * 3480 * 2). This is a real pre-upgrade account, not a hand-waved stub. - Layout assertions.
8 + FillStatusAccount::INIT_SPACE = 45,FilledRelay = 393bytes withfill_typeat offset 392,RequestedSlowFill = 280bytes — all match a hand-count of the struct fields. Event and account discriminators match thesha256("event:…")/sha256("account:…")derivations. - Tests. Ran CI's exact command,
cargo test -p svm-spoke --lib: 15/15 pass, and the three newtests::compatibility::*cases are included. With--features test: 19/19. Good call puttingmod testsbehind plain#[cfg(test)]rather than thetestfeature — CI (pr.yml:103) doesn't pass--features test, so feature-gating them would have made them dead weight. - No dangling references. Nothing in
src/,test/, orscripts/still importsSlowFillLeaf,slowFillHashFn,loadRequestSlowFillParams, or the removed account types. RemainingslowFillhits are either EVM (V3SlowFillinbuildSampleTree.ts) or event-name strings inqueryEvents*.ts, which stay valid. - The doc's
close_instruction_paramsclaim. Confirmed — it takes anUncheckedAccountand never deserializes, so stale slow-fill param buffers really can still be closed by their creator without the retired type. - AcrossPlus coverage lost with
SvmSpoke.SlowFill.AcrossPlus.tsis retained equivalently inSvmSpoke.Fill.AcrossPlus.ts(forwarding, max token distributions, both param modes).
The Anchor.toml glob quoting fix deserves a specific call-out: test/svm/fixtures/legacySlowFill.ts is the first .ts file under a test/svm/* subdirectory, and since sh expands ** as *, the unquoted pattern would have collapsed to just that fixture file and silently skipped the entire SVM suite. Catching that in the same PR that introduced the trigger is the difference between green CI and green-but-empty CI.
Not verified: I have no Solana validator here, so I could not reproduce the anchor test --skip-build run (147 passing) or the Docker verified build. Taking those on trust from the PR description.
Main item
The account/ABI compatibility story is documented exhaustively, but it's silent on the funding side of in-flight slow fills — see the inline comment on SLOW_FILL_RETIREMENT.md. It's a coordination question about upgrade ordering rather than a defect in this diff, which is why I'm commenting rather than blocking, but I'd like it addressed before deploy.
Remaining comments are nits.
| `RootBundle` retains both roots and its refund-claim bitmap. `relay_root_bundle` and its cross-chain admin payload | ||
| still accept both `relayer_refund_root` and `slow_relay_root`; the latter is stored and emitted but cannot be executed. | ||
| HubPool forwards the same global slow-relay root to every destination. A nonzero root may contain slow fills for | ||
| other chains, so rejecting it on Solana would also block delivery of the accompanying refund root. |
There was a problem hiding this comment.
This paragraph explains why a nonzero slow root is still accepted, which I agree with. What it doesn't cover is what happens to the LP funds backing slow-fill leaves that are already in flight when the upgrade lands.
As I understand the bundle flow, the dataworker builds slow-fill leaves from observed RequestedSlowFill events, and the pool rebalance leaf's netSendAmount for the destination chain is sized to cover them. So for any pre-upgrade RequestedSlowFill that hasn't been bundled yet, the sequence after this upgrade is: HubPool sends the funds to the Solana vault → the leaf is in the relayed slow root → and there is no longer an entrypoint that can execute it. The tokens sit in the vault, and the recipient never gets the slow fill.
Self-healing in principle (deposit expires → depositor refunded on the origin chain → excess returns via amount_to_return in a later bundle), but that depends on the dataworker actually handling it, and it isn't free.
Could you confirm the deployment sequencing? Specifically:
- Does the dataworker stop emitting SVM slow-fill leaves before or together with this upgrade, so the in-flight window is empty by the time the entrypoint disappears?
- If a nonempty window is unavoidable, is stranded vault balance reclaimed through the ordinary
amount_to_returnpath, or does it need a manual step?
Worth a short paragraph here either way — this doc is going to be the reference someone reaches for during the upgrade, and the funding side is the part that moves real money.
There was a problem hiding this comment.
Added the deployment requirements in 5d700387. This is part of the combined Lite-chain upgrade with #1548, which rejects nonzero amount_to_return and removes bridge_tokens_to_hub_pool. The suggested automatic return path therefore will not exist after that upgrade; an origin-chain expiry refund does not recover excess Solana vault funds.
The SDK excludes slow fills to/from Lite chains and requires token equivalence through pool-rebalance routes, so an old request event alone does not imply new funding. However, Lite-chain classification uses the deposit's quote timestamp. Before deployment we still need to verify active dataworker/configuration and reconcile older funded leaves, pending liabilities, and balances. Any residual obligations need settlement before the relevant paths disappear or a separately reviewed recovery procedure. This PR does not establish that the live in-flight window is empty.
Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖
| checks. Success transfers the original output amount, writes `Filled`, records the submitting relayer as rent | ||
| recipient, and emits `FilledRelay` with `ReplacedSlowFill`. Replay fails with `RelayFilled`. Any transaction failure | ||
| rolls back the token transfer and status change. Expired requests remain eligible for ordinary `close_fill_pda` | ||
| cleanup to their recorded rent recipient; they cannot be filled after expiry. |
There was a problem hiding this comment.
Two things I'd add to this paragraph, both about the pre-upgrade request that isn't fast-filled:
Depositor recourse. The doc says such a request "cannot be filled after expiry" but stops there. Worth stating plainly that the depositor's only remaining recourse is the origin-chain expiry refund — there is no on-Solana fallback at all now. That's the intended outcome of ACP-221, but a reader coming to this doc mid-incident shouldn't have to infer it.
Rent reclamation gap. close_fill_pda remains available to the recorded rent recipient, as you note. But scripts/svm/closeRelayerPdas.ts only enumerates filledRelay events, so it will never surface a stale RequestedSlowFill PDA. Those requesters have ~1.2M lamports each locked up with no tooling that finds it for them; they'd have to go through queryEvents.ts (which does list RequestedSlowFill) and construct the close call by hand. Probably a small follow-up rather than something for this PR, but it's worth a sentence here so the gap is on the record.
There was a problem hiding this comment.
Documented both in 5d700387: an unfilled expired deposit goes through the dataworker-driven origin-chain refund process, and closing its destination PDA only reclaims rent. The doc also explicitly records that closeRelayerPdas.ts discovers only filled relays; never-filled requests require separate discovery and a close call by the recorded rent recipient. No discovery-tooling change is included here.
Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖
| import { legacyMint, legacyRelay, legacyRequester } from "./fixtures/legacySlowFill"; | ||
| import { common } from "./SvmSpoke.common"; | ||
|
|
||
| describe("svm_spoke lite-chain compatibility", () => { |
There was a problem hiding this comment.
Naming nit, but I think it's worth changing before this lands.
"Lite chain" is an established Across term meaning a chain whose liquidity routes through the hub rather than being rebalanced independently — nothing to do with what this suite tests. This file is an upgrade/legacy-compatibility suite: retired selectors, historical event decoding, and fast-filling a frozen pre-upgrade account.
SvmSpoke.Compatibility.ts or SvmSpoke.LegacySlowFill.ts would say what it does. Note that SLOW_FILL_RETIREMENT.md:41 also calls it "the lite-chain suite," so the rename would want to touch both.
One related note: the suite consumes its genesis fixture (fills the account, then closes the PDA), so it is not re-runnable within a single validator session. Fine under anchor test, which starts fresh each time, but since the doc already tells people how to run this against a manually started validator, worth flagging there that it's single-shot.
There was a problem hiding this comment.
Renamed to SvmSpoke.SlowFillRetirement.ts and documented the fresh-validator requirement in 5d700387. The test body is unchanged.
One terminology correction: Lite chains exclude HubPool LP rebalancing. That is the intended Solana model, with #1548 handling token-rebalancing removal and this PR removing slow fills. The narrower suite name is still more accurate because these tests cover slow-fill retirement compatibility, not the entire combined upgrade.
Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖
| InvalidFillDeadline, | ||
| #[msg("Caller is not the exclusive relayer and exclusivity deadline has not passed!")] | ||
| NotExclusiveRelayer, | ||
| // Retain retired slow-fill error slots so subsequent error codes do not shift. |
There was a problem hiding this comment.
This comment reads as if it introduces a contiguous block of retired slots, but RelayFilled sits between the two retired variants and is very much still live — it's the replay guard in _fill, and the new LiteChain test asserts on it.
Suggest making it variant-scoped, matching the style you used two lines down:
// Historical slow-fill exclusivity error; no longer raised.
#[msg("The Deposit is still within the exclusivity window!")]
NoSlowFillsInExclusivityWindow,The "so subsequent error codes do not shift" rationale is the important part and is already captured in SLOW_FILL_RETIREMENT.md, so it doesn't need to be restated inline.
There was a problem hiding this comment.
Changed to the suggested variant-scoped comment in 5d700387. RelayFilled remains the active replay error; no error values changed.
Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖
droplet-rl
left a comment
There was a problem hiding this comment.
Re-reviewed at 5d700387. All four items addressed, and one of my premises was wrong in a way that matters — thanks for the correction. Approving.
Verification of the follow-up commit
- Rename. Confirmed a pure rename:
git diff -Mbetween559a391eand5d700387shows exactly one content line changed in the file (thedescribestring). No staleLiteChainreferences anywhere in.ts/.md/.toml/.yml, and the doc's two references were updated along with the new fresh-validator note. error.rs. Variant-scoped comment applied; no error values or ordering changed. Re-ran CI'scargo test -p svm-spoke --libat the new HEAD: 15/15, and 19/19 with--features test.v5_errors_use_dedicated_rangestill passes, so the slot layout is intact.
On the deployment gate — you were right and I was wrong
I checked out sol-cctpv2 to verify the claim rather than take it on trust, and it holds:
bundle.rs:116rejects nonzeroamount_to_returnwithNonZeroAmountToReturn.bridge_tokens_to_hub_poolis gone fromprograms/svm-spoke/srcon that branch entirely, while it still exists on this branch (token_bridge.rs,lib.rs).
So the "self-healing via amount_to_return in a later bundle" fallback I leaned on in my original comment will not exist after the combined upgrade. That makes the residual-funds question strictly more serious than I framed it, not less — there is no automatic recovery path on the far side. Documenting it as a hard pre-deployment gate, with the explicit "the compatibility tests do not establish that the live in-flight window is empty" disclaimer, is the right resolution. That reconciliation is genuinely not something this diff can settle.
I'll also own the terminology error: I glossed Lite chains as routing liquidity through the hub, which is backwards. Your definition — excluded from HubPool LP rebalancing — is what #1548 actually implements. Your point that the narrower suite name is still the better one stands regardless, and the rename is an improvement on both counts.
Summary
The diff itself was clean on first read and remains so. Everything I could check mechanically checks out: the retired selectors are the real sha256("global:…")[..8] values landing on Anchor's default dispatch arm with no #[fallback] in the way, the frozen fixture is a genuine 45-byte pre-upgrade account at the exact rent-exempt minimum, the layout assertions match a hand-count of the struct fields, and the Anchor.toml glob quoting prevents a silent full-suite skip that the new fixtures/ directory would otherwise have triggered.
Still not independently verified, as before: anchor test --skip-build (147 passing) and the Docker verified build — no validator available in my environment.
Approving. Note for anyone reading later: the pre-deployment reconciliation in the new "Lite-chain transition and existing funds" section is a blocking prerequisite for the combined upgrade, not for merging this PR into its stack base.
|
|
||
| The token-rebalancing restrictions are a separate upgrade change. In the combined deployment, refund leaves | ||
| must have `amount_to_return == 0` and `bridge_tokens_to_hub_pool` is removed. Consequently, ordinary | ||
| `amount_to_return` processing cannot recover residual Solana vault funds after that upgrade. |
There was a problem hiding this comment.
Verified this against sol-cctpv2 rather than taking it on trust, and it's accurate:
programs/svm-spoke/src/instructions/bundle.rs:116—if relayer_refund_leaf.amount_to_return != 0 { return err!(SvmError::NonZeroAmountToReturn); }bridge_tokens_to_hub_poolreturns no hits underprograms/svm-spoke/srcon that branch, while it's still present on this one.
Recording it here because this paragraph is the load-bearing one for the deployment gate: it's what removes the automatic recovery path a reader might otherwise assume exists (I assumed exactly that in my first review). Anyone evaluating residual vault balances before the combined upgrade should start from this paragraph and treat the reconciliation below as blocking — there is no second chance via amount_to_return afterward.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
638afe1 to
0c25db4
Compare
f7f705a to
608c7db
Compare
5052d26 to
e5dd925
Compare
608c7db to
bc5c128
Compare
e5dd925 to
3f78703
Compare
bc5c128 to
0a20d28
Compare
3f78703 to
e8c4ad5
Compare
0a20d28 to
d677b2d
Compare
e8c4ad5 to
b7cfafe
Compare
4857e94 to
f38ffb8
Compare
8dad3f4 to
f04c2c3
Compare
f38ffb8 to
208fe5a
Compare
f04c2c3 to
fe3a55d
Compare
208fe5a to
ab766c9
Compare
fe3a55d to
50fb8e3
Compare
ab766c9 to
efb0124
Compare
efb0124 to
dc0bd72
Compare
50fb8e3 to
2215794
Compare
dc0bd72 to
db71398
Compare
2215794 to
d646de9
Compare
db71398 to
5c62e14
Compare
d646de9 to
23277c0
Compare
| // Deterministic test keys, not production credentials. The matching fill-status account is loaded at genesis. | ||
| export const legacyMint = Keypair.fromSeed(Buffer.alloc(32, 221)); | ||
| export const legacyRequester = Keypair.fromSeed(Buffer.alloc(32, 222)).publicKey; | ||
| export const legacyRelay: RelayData = { |
There was a problem hiding this comment.
Could we inline these constants into SvmSpoke.SlowFillRetirement.ts and remove this file? There is one consumer, and this small module only defines the deterministic relay and keys matching the frozen account fixture. Keeping them beside the test makes that relationship easier to follow without another import/file boundary.
The frozen JSON account serves a separate purpose and should remain while we test historical-account compatibility: constructing its bytes with the current encoder during the test could hide a layout regression.
There was a problem hiding this comment.
Done in b4f9f57b: inlined the constants beside the test and removed the helper file. The frozen JSON and test body are unchanged, with fixture provenance and the fresh-validator requirement now beside the constants. Carried this through the stack while preserving the later expiry-reclaim test.
Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖
| @@ -0,0 +1,71 @@ | |||
| # SVM slow-fill retirement | |||
There was a problem hiding this comment.
Could we consolidate this into the existing documentation and remove the standalone retirement document before merging? The useful content has three natural homes:
- Permanent account-layout, enum-position, and two-root admin ABI constraints: inline comments and the existing adapter spec.
- Outstanding requests, funded leaves, and residual vault balances: the combined upgrade's deployment runbook. These deployment requirements should be preserved.
- Fixture provenance and the fresh-validator requirement: beside the test fixture.
That keeps the enduring compatibility rules close to the code and the one-time transition requirements with the deployment, without maintaining another overlapping document. Please update the links to this file as part of the consolidation.
There was a problem hiding this comment.
Consolidated in b4f9f57b: compatibility rules are in the existing adapter spec, deployment requirements are under its Deployment sequencing section, and fixture notes are beside the test. Removed the standalone doc and updated its links. Restacked through #1565, folding the deployment notes into the existing combined-upgrade section and preserving the V5-only wording after legacy entrypoint retirement.
Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖
| there is no destination slow-fill fallback. Closing the fill-status PDA only reclaims rent and does not refund | ||
| the deposit. `scripts/svm/closeRelayerPdas.ts` discovers accounts from `FilledRelay` events only, so it does not | ||
| find requests that were never filled. Those accounts require separate discovery and a `close_fill_pda` call | ||
| by their recorded rent recipient after expiry. |
There was a problem hiding this comment.
Small wording correction to preserve wherever this paragraph ends up: close_fill_pda is permissionless. The account named signer is an UncheckedAccount constrained to the recorded rent recipient; it does not require that recipient's signature. Anyone can submit the close after expiry, but the rent must go to the recorded recipient. Suggest replacing "by their recorded rent recipient after expiry" with "after expiry, returning rent to their recorded rent recipient".
There was a problem hiding this comment.
Corrected in b4f9f57b: anyone may submit the close after expiry; rent goes to the recorded recipient. This is now explicit in the adapter spec. My earlier reply here also incorrectly said the recipient had to submit it. No contract behavior changed.
Sent from Reinis Martinsons's Codex CLI Agent using gpt-6-astra 🤖
grasphoper
left a comment
There was a problem hiding this comment.
LGTM, left a few comments
5c62e14 to
dc7d342
Compare
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
b4f9f57 to
30d8928
Compare
Retires Solana slow fills by removing
request_slow_fill,execute_slow_relay_leaf, and their client builders. Historical selectors now fail at dispatch; existing requested relays can still be fast-filled withReplacedSlowFill, with replay and rollback protection preserved.Preserves serialized account layouts, enum/error slots, historical event decoding, and the two-root admin ABI. Nonzero slow roots remain accepted because HubPool shares the root across destinations; rejecting one could block valid Solana refunds when other chains have slow fills.
Adds raw-selector and upgrade-compatibility tests using a frozen pre-upgrade account, updates the compatibility docs, and quotes the Anchor test glob so fixture subdirectories cannot cause the suite to be skipped.
Based on #1544. CCTP v2/token-rebalance removal remains in #1548; the shared upgrade candidate must also verify its nonzero
amount_to_returnguard.Validation completed locally using CI's Anchor 0.31.1 / Solana 2.2.1 toolchain:
anchor test --skip-build: 147 passing.Resolves ACP-221.