Solana → EVM transfer fails with 'Signature has expired: block height exceeded' + Solana balance deducted on failed retry #128

Open
opened 2026-05-01 06:50:08 +00:00 by Brouie · 11 comments
Brouie commented 2026-05-01 06:50:08 +00:00 (Migrated from gitlab.com)

Solana → MegaETH transfers fail repeatedly with expired blockhash. Frontend appears to reuse stale blockhash on retry instead of refreshing. Worse: balance is deducted on the failed retry attempt — Solana TDEC went 0.99 → deducted post-fail with no destination receipt.

Repro 2026-05-01, wallet Backpack Cu6Q...aDBy, dest 0xc46b15f4B56489a16F561c22D5F0BA8bdCa80650, dest chain MegaETH:

  • testa attempt 1: Signature 2GLYknGGg8WG7QwimJL9... has expired: block height exceeded
  • testa attempt 2 retry: Signature 3fqggsoFUprTQ1vuZmHonr... has expired: block height exceeded
  • tdec attempt: Signature 4wwWRCsbjRAFZp3bdJGSfz9MqXNT6AN5Bjbvdm1zyGkBAgL6rZm6LH... has expired: block height exceeded — and Solana TDEC balance dropped 0.99 → deducted with no destination receipt seen

Solana RPC was responsive same session (balance reads worked, prior outbound rows from EVM → Solana also worked). Looks like a frontend retry path issue.

P1-high — silent loss on failed retry is more than a UX issue. cc @PlasticDigits

Solana → MegaETH transfers fail repeatedly with expired blockhash. Frontend appears to reuse stale blockhash on retry instead of refreshing. **Worse: balance is deducted on the failed retry attempt** — Solana TDEC went 0.99 → deducted post-fail with no destination receipt. Repro 2026-05-01, wallet Backpack Cu6Q...aDBy, dest 0xc46b15f4B56489a16F561c22D5F0BA8bdCa80650, dest chain MegaETH: - testa attempt 1: Signature 2GLYknGGg8WG7QwimJL9... has expired: block height exceeded - testa attempt 2 retry: Signature 3fqggsoFUprTQ1vuZmHonr... has expired: block height exceeded - tdec attempt: Signature 4wwWRCsbjRAFZp3bdJGSfz9MqXNT6AN5Bjbvdm1zyGkBAgL6rZm6LH... has expired: block height exceeded — **and Solana TDEC balance dropped 0.99 → deducted with no destination receipt seen** Solana RPC was responsive same session (balance reads worked, prior outbound rows from EVM → Solana also worked). Looks like a frontend retry path issue. P1-high — silent loss on failed retry is more than a UX issue. cc @PlasticDigits
PlasticDigits commented 2026-05-01 07:04:41 +00:00 (Migrated from gitlab.com)

mentioned in commit b6c5b5afdc

mentioned in commit b6c5b5afdcf85679520fa63e75bc4c429d42a73e
PlasticDigits commented 2026-05-01 07:04:58 +00:00 (Migrated from gitlab.com)

GL-128 fix merged to main (commit b6c5b5a)

What changed

  • sendSolanaTransaction (packages/frontend/src/services/solana/transaction.ts) now copies the instruction list once, then each wallet path (signAndSendTransaction vs signTransaction → simulate → sendRawTransaction) builds a new legacy Transaction and fetches a fresh getLatestBlockhash immediately before that path runs. Previously the same recentBlockhash / lastValidBlockHeight pair was applied once and reused when falling back between paths after wallet or RPC delay, which matches the repro (Signature has expired: block height exceeded on retries).
  • looksLikeSolanaExpiredBlockhashError — shared classifier for logs/support.
  • Documentation: INV-FE-SOLANA-BH1 in docs/FRONTEND_BRIDGE_INVARIANTS.md, cross-links in docs/SOLANA_BRIDGE_INVARIANTS.md, solanaRpcUrls.ts header, new skills/agent-solana-tx-blockhash.md, link from skills/agent-frontend-bridge-chains.md.
  • Tests: transaction.test.ts cases for the classifier.

What this does not automatically solve

  • If a deposit actually confirmed on-chain but the UI errored afterward, a second form submit still runs fetchDepositNonce again and could create another deposit. Operators should confirm via explorer before retrying; the new skill doc calls this out.

Verification checklist for @brouie

  1. Solana → MegaETH (or other EVM): submit a small SPL deposit; if the wallet first hits signAndSend and falls back (or vice versa), confirm the flow completes without block height exceeded where it previously failed on retry.
  2. Backpack (repro wallet): repeat with the same route; ensure no stale-blockhash error after dismissal / second path.
  3. RPC: keep using bridge-configured RPCs; confirm confirmTransaction still reaches confirmed on first success path.
  4. Regression: withdraw_submit / Solana withdraw execute / faucet panel paths that call sendSolanaTransaction still sign and confirm.
  5. Optional: VITE_SOLANA_PREFER_SIGN_AND_SEND_FIRST=true — exercise both orderings; each path should show a new blockhash (behavioral; no extra UI).

Leaving this issue open until you sign off on mainnet QA.

## GL-128 fix merged to main (commit b6c5b5a) ### What changed - **`sendSolanaTransaction`** (`packages/frontend/src/services/solana/transaction.ts`) now **copies the instruction list once**, then **each** wallet path (`signAndSendTransaction` vs `signTransaction` → simulate → `sendRawTransaction`) builds a **new** legacy `Transaction` and fetches a **fresh** `getLatestBlockhash` immediately before that path runs. Previously the same `recentBlockhash` / `lastValidBlockHeight` pair was applied once and reused when falling back between paths after wallet or RPC delay, which matches the repro (**Signature has expired: block height exceeded** on retries). - **`looksLikeSolanaExpiredBlockhashError`** — shared classifier for logs/support. - **Documentation:** **INV-FE-SOLANA-BH1** in `docs/FRONTEND_BRIDGE_INVARIANTS.md`, cross-links in `docs/SOLANA_BRIDGE_INVARIANTS.md`, `solanaRpcUrls.ts` header, new **`skills/agent-solana-tx-blockhash.md`**, link from `skills/agent-frontend-bridge-chains.md`. - **Tests:** `transaction.test.ts` cases for the classifier. ### What this does *not* automatically solve - If a deposit **actually confirmed** on-chain but the UI errored afterward, a **second** form submit still runs `fetchDepositNonce` again and could create another deposit. Operators should confirm via explorer before retrying; the new skill doc calls this out. ### Verification checklist for @brouie 1. **Solana → MegaETH (or other EVM)**: submit a small SPL deposit; if the wallet first hits `signAndSend` and falls back (or vice versa), confirm the flow completes without **block height exceeded** where it previously failed on retry. 2. **Backpack** (repro wallet): repeat with the same route; ensure no stale-blockhash error after dismissal / second path. 3. **RPC**: keep using bridge-configured RPCs; confirm `confirmTransaction` still reaches **confirmed** on first success path. 4. **Regression**: `withdraw_submit` / Solana withdraw execute / faucet panel paths that call `sendSolanaTransaction` still sign and confirm. 5. Optional: **`VITE_SOLANA_PREFER_SIGN_AND_SEND_FIRST=true`** — exercise both orderings; each path should show a **new** blockhash (behavioral; no extra UI). Leaving this issue **open** until you sign off on mainnet QA.
Brouie commented 2026-05-01 07:58:33 +00:00 (Migrated from gitlab.com)

mentioned in issue #123

mentioned in issue #123
Brouie commented 2026-05-01 15:54:32 +00:00 (Migrated from gitlab.com)

Source-level pre-walkthrough on b6c5b5a. Architecture is correct — stale-blockhash class of bug cannot recur on this code path.

Implementation review

  • services/solana/transaction.ts:651 — instructions = transaction.instructions.slice() copies the instruction list once at entry.
  • Lines 652-672 — prepareFreshTransaction closure: each call rebuilds a new Transaction from the copied instructions and fetches a fresh getLatestBlockhash snapshot. Returns { tx, blockhash, lastValidBlockHeight }.
  • Both signing paths (signAndSendTransaction and signTransaction + sendRawTransaction) call prepareFreshTransaction() independently, so each path gets its own blockhash. Reuse architecturally not possible.
  • INV-FE-SOLANA-BH1 documented inline in the JSDoc with (GL-128) reference.
  • VITE_SOLANA_PREFER_SIGN_AND_SEND_FIRST env flag (line 675) controls path ordering — exposes both orderings for testing item 5.

Classifier review

looksLikeSolanaExpiredBlockhashError (line 309) covers 7 error-message variants:

  • block height exceeded (matches my repro literally)
  • blockheightexceeded (no-space variant)
  • transactionexpired
  • signature has expired (also in my repro)
  • transaction expired
  • expired blockhash
  • blockhash not found

Defensive type handling: Error / string / arbitrary objects (lazy JSON.stringify IIFE).

Tests

  • npx vitest run solana/transaction.test: 8/8 PASS in 6ms (1 dedicated GL-128 classifier test at line 52).
  • Full bridge suite: 638/638 PASS in 38s (re-run from #127 verification — covered both fixes since eaa3d0a and b6c5b5a both ship in 27a5e42 HEAD).

Re: the silent-loss caveat

Your comment notes: "If a deposit actually confirmed on-chain but the UI errored afterward, a second form submit still runs fetchDepositNonce again and could create another deposit. Operators should confirm via explorer before retrying." That maps exactly to my repro's tdec balance loss (0.99 → deducted with no destination receipt). Acknowledging the operator-process mitigation is acceptable for now via the new skills/agent-solana-tx-blockhash.md. Will follow your guidance and confirm via explorer before any retry on Solana → EVM going forward.

cc @PlasticDigits

Source-level pre-walkthrough on `b6c5b5a`. Architecture is correct — stale-blockhash class of bug cannot recur on this code path. ### Implementation review - `services/solana/transaction.ts:651` — `instructions = transaction.instructions.slice()` copies the instruction list once at entry. - Lines 652-672 — `prepareFreshTransaction` closure: each call **rebuilds a new `Transaction`** from the copied instructions and **fetches a fresh `getLatestBlockhash`** snapshot. Returns `{ tx, blockhash, lastValidBlockHeight }`. - Both signing paths (`signAndSendTransaction` and `signTransaction` + `sendRawTransaction`) call `prepareFreshTransaction()` independently, so each path gets its own blockhash. Reuse architecturally not possible. - INV-FE-SOLANA-BH1 documented inline in the JSDoc with `(GL-128)` reference. - `VITE_SOLANA_PREFER_SIGN_AND_SEND_FIRST` env flag (line 675) controls path ordering — exposes both orderings for testing item 5. ### Classifier review `looksLikeSolanaExpiredBlockhashError` (line 309) covers 7 error-message variants: - `block height exceeded` (matches my repro literally) - `blockheightexceeded` (no-space variant) - `transactionexpired` - `signature has expired` (also in my repro) - `transaction expired` - `expired blockhash` - `blockhash not found` Defensive type handling: `Error` / `string` / arbitrary objects (lazy `JSON.stringify` IIFE). ### Tests - `npx vitest run solana/transaction.test`: 8/8 PASS in 6ms (1 dedicated GL-128 classifier test at line 52). - Full bridge suite: 638/638 PASS in 38s (re-run from #127 verification — covered both fixes since `eaa3d0a` and `b6c5b5a` both ship in `27a5e42` HEAD). ### Re: the silent-loss caveat Your comment notes: *"If a deposit actually confirmed on-chain but the UI errored afterward, a second form submit still runs `fetchDepositNonce` again and could create another deposit. Operators should confirm via explorer before retrying."* That maps exactly to my repro's tdec balance loss (0.99 → deducted with no destination receipt). Acknowledging the operator-process mitigation is acceptable for now via the new `skills/agent-solana-tx-blockhash.md`. Will follow your guidance and confirm via explorer before any retry on Solana → EVM going forward. cc @PlasticDigits
Brouie commented 2026-05-02 00:58:20 +00:00 (Migrated from gitlab.com)

mentioned in issue #131

mentioned in issue #131
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-05-02 04:29:51 +00:00
Brouie commented 2026-05-03 22:10:02 +00:00 (Migrated from gitlab.com)

mentioned in issue #127

mentioned in issue #127
Brouie commented 2026-05-03 22:10:03 +00:00 (Migrated from gitlab.com)

Production deploy gap — sign-off blocked

Verified prod bundle on bridge.cl8y.com today: footer reads v0.1.345 · 27a5e42. Repo main HEAD is currently at 95f8fd5, which is the merge commit containing all four fixes (#127 eaa3d0a, #128 b6c5b5a, #130 Terra || clause drop, #131 67a48e4). Prod is therefore still on the pre-fix bundle and none of the four fixes are live yet.

Confirming repro on prod (proves bundle is pre-fix):

  • EVM wallet chip in bridge header shows ETH text fallback while MetaMask is on MegaETH (chain 4326). This is the clean #131-D pre-fix behavior — getChainLogoPath has no branch for 4326, falls through to getGasSymbol default ETH.

Cannot run mainnet sign-off on the verification checklist for #127 / #128 / #130 / #131 until prod cuts to current HEAD (or whichever release commit contains the four fixes).

Will re-run the combined live walk on bridge.cl8y.com once deploy is confirmed:

  • #128 — Solana → MegaETH × 3 (testa / testb / tdec) for fresh-blockhash retry path
  • #127 — deliberate rate-limit row, observe amber "Resets in" countdown banner
  • #130 — MegaETH → Terra tdec 0.001, expect COMPLETE clean
  • #131 — A (stepper stable across poll), B (Switch button visible on wrong chain), C (no refresh stall after switch), D (mega.png header chip)

Plus the 4 #123 blocked rows pick-up that rides on #128 / #130 fixes.

cc @PlasticDigits

## Production deploy gap — sign-off blocked Verified prod bundle on bridge.cl8y.com today: footer reads `v0.1.345 · 27a5e42`. Repo `main` HEAD is currently at `95f8fd5`, which is the merge commit containing all four fixes (#127 `eaa3d0a`, #128 `b6c5b5a`, #130 Terra `||` clause drop, #131 `67a48e4`). Prod is therefore still on the pre-fix bundle and none of the four fixes are live yet. Confirming repro on prod (proves bundle is pre-fix): - EVM wallet chip in bridge header shows `ETH` text fallback while MetaMask is on MegaETH (chain 4326). This is the clean #131-D pre-fix behavior — `getChainLogoPath` has no branch for 4326, falls through to `getGasSymbol` default `ETH`. Cannot run mainnet sign-off on the verification checklist for #127 / #128 / #130 / #131 until prod cuts to current HEAD (or whichever release commit contains the four fixes). Will re-run the combined live walk on bridge.cl8y.com once deploy is confirmed: - #128 — Solana → MegaETH × 3 (testa / testb / tdec) for fresh-blockhash retry path - #127 — deliberate rate-limit row, observe amber "Resets in" countdown banner - #130 — MegaETH → Terra tdec 0.001, expect COMPLETE clean - #131 — A (stepper stable across poll), B (Switch button visible on wrong chain), C (no refresh stall after switch), D (mega.png header chip) Plus the 4 #123 blocked rows pick-up that rides on #128 / #130 fixes. cc @PlasticDigits
Brouie commented 2026-05-03 22:10:05 +00:00 (Migrated from gitlab.com)

mentioned in issue #130

mentioned in issue #130
PlasticDigits (Migrated from gitlab.com) reopened this issue 2026-05-04 03:34:52 +00:00
PlasticDigits commented 2026-05-04 03:38:51 +00:00 (Migrated from gitlab.com)

@Brouie bridge.cl8y.com frontend is now deployed at latest commit.

@Brouie bridge.cl8y.com frontend is now deployed at latest commit.
Brouie commented 2026-05-07 05:43:14 +00:00 (Migrated from gitlab.com)

@PlasticDigits — ack on the prod deploy. queueing the bundled bridge live-walk for the next mainnet session (need real Backpack + MetaMask + small mainnet balances). will cover the full #127 / #128 / #130 / #131 verification matrix in one pass + pick up the #123 blocked rows that ride on the fixes. apologies for the delay — yieldomega QA + MR work has been priority this week.

reasonable target: by Monday 2026-05-11. flagging if anything blocks earlier.

@PlasticDigits — ack on the prod deploy. queueing the bundled bridge live-walk for the next mainnet session (need real Backpack + MetaMask + small mainnet balances). will cover the full #127 / #128 / #130 / #131 verification matrix in one pass + pick up the #123 blocked rows that ride on the fixes. apologies for the delay — yieldomega QA + MR work has been priority this week. reasonable target: by Monday 2026-05-11. flagging if anything blocks earlier.
Brouie commented 2026-05-07 05:44:43 +00:00 (Migrated from gitlab.com)

mentioned in issue #118

mentioned in issue #118
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-bridge-monorepo#128
No description provided.