Skip to content

improve(svm): Replace type assertions with real narrowing - #1507

Merged
pxrl merged 3 commits into
masterfrom
pxrl/svm1
Aug 26, 2026
Merged

improve(svm): Replace type assertions with real narrowing#1507
pxrl merged 3 commits into
masterfrom
pxrl/svm1

Conversation

@pxrl

@pxrl pxrl commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Replace lying casts/guards in the SVM event pipeline with honest predicates. Behaviour changes:

  • findFillEvent: populate blockNumber/txnRef from the tx envelope (were undefined at runtime).
  • getEventName: exact match; unknown event names now throw at the query boundary.

Groundwork for typed SVM event decoding (codama decoders follow separately).

Replace lying casts/guards in the SVM event pipeline with honest predicates
(superstruct where structural). Behaviour changes:
 - findFillEvent: populate blockNumber/txnRef from the tx envelope (were undefined at runtime).
 - getEventName: exact match; unknown event names now throw at the query boundary.
 - findDeposit: skip malformed events rather than throwing mid-search.

Groundwork for typed SVM event decoding (codama decoders follow separately).
@pxrl

pxrl commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0a11392587

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/arch/svm/utils.ts Outdated
`name in SVMEventNames` also matches keys inherited from Object.prototype
("toString", "constructor", ...), so getEventName returned those as valid
event names instead of throwing. Use Object.hasOwn for an own-property check.

Adds a unit test covering the declared names, the inherited keys, and the
substring near-misses the previous `includes()` matcher accepted.

Co-Authored-By: Claude <noreply@anthropic.com>
@droplet-rl

Copy link
Copy Markdown
Contributor

🔎 View trace

@droplet-rl

Copy link
Copy Markdown
Contributor

isUint8Array keeps its name but inverts its meaning.

src/utils/ArrayUtils.ts:191 — the declared type goes from value is Uint8Array to value is number[], and the body deliberately does not match Uint8Array instances (the new doc comment says so explicitly). So isUint8Array(new Uint8Array(32)) now returns false.

It's publicly exported (src/utils/index.ts./ArrayUtils), so this is an API-surface change: an external if (isUint8Array(x)) { x.byteLength } stops compiling, and anyone using it to detect real byte arrays silently gets the wrong answer.

Given the stack's premise is removing type lies, a predicate named isUint8Array that rejects Uint8Array seems like the wrong end state. Renaming looks free — no in-repo callers remain (only isByteArray at :181 uses it), and GitHub code search finds 0 hits in across-protocol/relayer. Something like isByteNumberArray would say what it does.

@droplet-rl

Copy link
Copy Markdown
Contributor

logIndex: 0, txnIndex: 0 placeholders in findFillEvent, and the collision with #1490.

src/arch/svm/SpokeUtils.ts:492. To be clear this PR is a net improvement — before it, unwrapEventData<SortableEvent>(rawEvent.data, ...) sourced SortableEvent fields from event data that never contained them, so findFillEvent returned a FillWithBlock with blockNumber/txnRef undefined. Sourcing them from the transaction envelope fixes that.

The 0 placeholders are the part worth sequencing. #1513 in this stack is premised on batched fills emitting several FilledRelay events in one transaction — and in exactly that case the fills share txnRef and all get logIndex: 0, so anything downstream keying on (txnRef, logIndex) collides.

md0x's #1490 ("preserve event log indices") assigns the real inner-instruction ordinal and is the proper fix. It also touches eventsClient.ts, types.ts, SpokeUtils.ts, MockSvmCpiEventsClient.ts and MockSvmSpokePoolClient.ts — all rewritten substantially by this stack — so it will conflict hard whichever lands second. Worth agreeing an order.

Remove the superstruct DepositIdEventData guard and the isByteArray/isUint8Array
ArrayUtils split. These hardened consumers of untyped decode output; the typed
codama decoders (next PR) fix the producer instead, and the final cleanup PR
was deleting this machinery again. Dropping it keeps each rung of the stack
touching this surface once. Also reverts an unrelated yarn.lock pin change.
@droplet-rl

Copy link
Copy Markdown
Contributor

Re-reviewed at af6cb87. The two things I raised earlier are resolved: isUint8Array went away with the ArrayUtils revert, and the in-vs-hasOwn guard landed in 6d0825c. Revert is clean — no dangling superstruct/isByteArray references, no dependency drift, merges cleanly onto master.

Verified locally (installed solana-test-validator, so the suites CI runs were all exercised): full suite 460 passing, tsc --noEmit on both configs clean, eslint clean.

The findFillEvent fix has no test.

I confirmed the bug is real by probing the returned fill on both sides of the change:

master:  blockNumber=undefined txnRef=undefined txnIndex=undefined logIndex=undefined
this PR: blockNumber=39        txnRef="3ipTTJWQ…mxxjH"    txnIndex=0     logIndex=0

SVMSpokePoolClient: Fills reports 6 passing in both cases. The only assertion on the returned fill is expect(fill.depositId) (test/SVMSpokePoolClient.fills.ts:113), so nothing pins the envelope fields — the fix is free to regress silently. The new Solana.getEventName.unit.test.ts covers the lower-risk half of the PR; the half that changes runtime output is uncovered.

This matters because findSvmFillEvent feeds the dataworker prefill path (BundleDataClient.ts:1067), so a FillWithBlock with blockNumber: undefined was reaching bundle construction. To be fair on blast radius: I went looking for a concrete break and didn't find one on today's code — the fill.txnRef read in verifyFillRepayment is behind chainIsEvm(fill.destinationChainId), and batchComputeRealizedLpFeePct keys off quoteTimestamp. So this was latent rather than actively burning us. Still worth two lines:

expect(fill.depositId).to.equal(BigNumber.from(relayData.depositId));
// SortableEvent fields are sourced from the tx envelope, not event data.
expect(fill.blockNumber).to.be.greaterThan(0);
expect(fill.txnRef).to.equal(fillSignature);

Checks that came back clean, recording them so they don't get re-litigated:

  • getEventName exact-match is safe against the real IDL. svm_spoke.json declares exactly the 13 names in SVMEventNames, and since it's a string enum, keys === values, so Object.hasOwn is equivalent to the old Object.values(...) scan for every legitimate name.
  • Safe for the one downstream caller. across-protocol/relayer calls arch.svm.getEventName in src/libexec/util/svm/util.ts:32; the names originate as literals in RelayerSpokePoolListenerSVM.ts (["FundsDeposited", "FilledRelay", "RelayedRootBundle", "ExecutedRelayerRefundRoot"]). All exact. The in-SDK call site is already gated by _queryableEventNames(), so it's a belt-and-braces re-check.
  • toAddress swapping the cast for address() can't newly throw. I half-expected an EvmAddress to blow up here, but Address's constructor zero-pads rawAddress to 32 bytes, so toBase58() always yields 32–44 base58 chars — inside what address() accepts. Confirmed at runtime for both 20- and 32-byte EVM inputs.
  • Envelope fields match the convention already used at SpokeUtils.ts:283-290, SVMSpokePoolClient.ts:160-163 and eventsClient.ts:304-306.

Still open from last round: the txnIndex: 0/logIndex: 0 sequencing against md0x's #1490. #1490 hasn't moved since 2026-07-17 and #1513 is still open, so the collision I flagged is unresolved rather than decided. Not a blocker for this PR — just needs an owner picking an order.

@Reinis-FRP Reinis-FRP 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.

LGTM - scoped, correct, and well-tested

@pxrl
pxrl merged commit 89dd86d into master Aug 26, 2026
4 checks passed
@pxrl
pxrl deleted the pxrl/svm1 branch August 26, 2026 11:07
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.

3 participants