Skip to content

fix(node): handle skipped Solana slots as unavailable blocks#35

Open
OkekeChristian24 wants to merge 1 commit into
subquery:mainfrom
OkekeChristian24:fix-skipped-slots
Open

fix(node): handle skipped Solana slots as unavailable blocks#35
OkekeChristian24 wants to merge 1 commit into
subquery:mainfrom
OkekeChristian24:fix-skipped-slots

Conversation

@OkekeChristian24

@OkekeChristian24 OkekeChristian24 commented Jul 3, 2026

Copy link
Copy Markdown

Description

This PR fixes an issue where the Solana node can get stuck retrying a skipped slot.

Solana slots can be skipped as part of normal chain behavior. When the node attempts to fetch one of those slots, the Solana RPC may return an error like:

Slot 394555688 was skipped, or missing due to ledger jump to recent snapshot

Fixes # 34

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

Checklist

  • I have tested locally
  • I have performed a self review of my changes
  • Updated any relevant documentation
  • Linked to any relevant issues
  • I have added tests relevant to my changes
  • Any dependent changes have been merged and published in downstream modules
  • My code is up to date with the base branch
  • I have updated relevant changelogs. We suggest using chan

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of skipped Solana slots so unavailable blocks now return a clear error instead of failing inconsistently.
    • Block headers now use the expected slot height when available, making block height reporting more accurate.
  • Tests

    • Added coverage for skipped-slot error handling and block/header height behavior.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds handling for skipped Solana slots: SolanaApi.handleError now detects a skipped-slot RPC error message and throws BlockUnavailableError. Block transformation utilities (transformBlock, formatBlockUtil, solanaBlockToHeader) gain an optional slot parameter, stored via a new internal SLOT symbol and used to compute blockHeight. Tests are added across API and block utility specs.

Changes

Skipped slot handling

Layer / File(s) Summary
Slot propagation through block transformation and header utilities
packages/node/src/solana/block.solana.ts, packages/node/src/solana/block.solana.spec.ts
Adds a SLOT symbol stored on transformed blocks, extends transformBlock, formatBlockUtil, and solanaBlockToHeader signatures to accept optional slot, prioritized over parent-slot fallback for blockHeight; adds unit tests verifying slot-driven header height.
SolanaApi skipped-slot error mapping and wiring
packages/node/src/solana/api.solana.ts, packages/node/src/solana/api.solana.unit.spec.ts, packages/node/src/solana/api.solana.spec.ts
Adds SKIPPED_SLOT_ERROR detection in handleError mapping to BlockUnavailableError, refactors getHeaderByHeight with try/catch and slot passing, updates fetchBlock to pass blockNumber through transformBlock/formatBlockUtil, removes unused delay import, and adds tests covering skipped-slot error propagation.

Estimated code review effort: 2 (Simple) | ~15 minutes

Possibly related issues

Possibly related PRs

  • subquery/subql-solana#30: Both PRs modify Solana skipped-slot handling by mapping RPC errors to BlockUnavailableError and propagate the requested slot into solanaBlockToHeader/block formatting.
  • subquery/subql-solana#32: Both PRs update api.solana.ts to classify skipped-slot RPC failures and route them through handleError into BlockUnavailableError, with accompanying tests.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main fix: treating skipped Solana slots as unavailable blocks.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix-skipped-slots

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/node/src/solana/block.solana.spec.ts (2)

21-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assertion on Line 25 doesn't test what it claims.

Object.getOwnPropertyNames never returns Symbol keys regardless of whether the property is enumerable, so this assertion would pass even if the SLOT property were enumerable/writable. It doesn't actually verify the "hidden" storage behavior.

✅ Suggested fix using the actual Symbol/serialization checks
     expect(solanaBlockToHeader(block).blockHeight).toBe(15);
-    expect(Object.getOwnPropertyNames(block)).not.toContain('slot');
+    expect(JSON.stringify(block)).not.toContain('"slot"');
+    expect(Object.keys(block)).not.toContain('slot');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/node/src/solana/block.solana.spec.ts` around lines 21 - 26, The test
in solanaBlockToHeader/transformBlock is only checking string-keyed property
names, so it does not validate that the SLOT storage is truly hidden. Update the
spec around transformBlock and solanaBlockToHeader to assert the actual
Symbol-based behavior for SLOT, and verify serialization/own-property visibility
using symbol-aware checks rather than Object.getOwnPropertyNames. Make sure the
test explicitly proves the slot is preserved in the transformed block while
remaining non-enumerable/non-string-keyed.

16-19: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Missing coverage for the parentSlot + 1 fallback branch.

This new spec file covers the explicit-slot and stored-SLOT-symbol branches of solanaBlockToHeader, but not the original fallback (Number(block.parentSlot) + 1) that still runs when no slot is available at all. Since this is a new file, that branch currently has zero coverage.

✅ Suggested additional test
   it('uses the fetched slot as the header height', () => {
     expect(solanaBlockToHeader(mockBlock, 15).blockHeight).toBe(15);
   });
+
+  it('falls back to parentSlot + 1 when no slot is available', () => {
+    expect(solanaBlockToHeader(mockBlock).blockHeight).toBe(13);
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/node/src/solana/block.solana.spec.ts` around lines 16 - 19, Add a
test in solanaBlockToHeader’s spec to cover the fallback path where no slot is
available and blockHeight is derived from Number(block.parentSlot) + 1. Extend
the Solana block utils describe block with a case that omits both the explicit
slot argument and the stored SLOT symbol, then assert the header height uses
parentSlot plus one. Use solanaBlockToHeader and the mockBlock setup already in
this spec to keep the test aligned with the existing branches.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/node/src/solana/api.solana.ts`:
- Line 31: Replace the brittle skipped-slot string check with Solana’s
structured error handling in api.solana.ts. Update the logic around the
skipped-slot handling to use the typed error path from
`@solana/kit/`@solana/errors, using isSolanaError(...) and the dedicated
skipped-slot error code instead of checking e.message.includes('was skipped').
Keep the SKIPPED_SLOT_ERROR symbol only if it is still needed for the structured
comparison, and make the retry/skip decision based on the typed error fields.

---

Nitpick comments:
In `@packages/node/src/solana/block.solana.spec.ts`:
- Around line 21-26: The test in solanaBlockToHeader/transformBlock is only
checking string-keyed property names, so it does not validate that the SLOT
storage is truly hidden. Update the spec around transformBlock and
solanaBlockToHeader to assert the actual Symbol-based behavior for SLOT, and
verify serialization/own-property visibility using symbol-aware checks rather
than Object.getOwnPropertyNames. Make sure the test explicitly proves the slot
is preserved in the transformed block while remaining
non-enumerable/non-string-keyed.
- Around line 16-19: Add a test in solanaBlockToHeader’s spec to cover the
fallback path where no slot is available and blockHeight is derived from
Number(block.parentSlot) + 1. Extend the Solana block utils describe block with
a case that omits both the explicit slot argument and the stored SLOT symbol,
then assert the header height uses parentSlot plus one. Use solanaBlockToHeader
and the mockBlock setup already in this spec to keep the test aligned with the
existing branches.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 99c0b80f-e745-4e98-a763-1acd2de71155

📥 Commits

Reviewing files that changed from the base of the PR and between c8896de and 97dc34c.

📒 Files selected for processing (5)
  • packages/node/src/solana/api.solana.spec.ts
  • packages/node/src/solana/api.solana.ts
  • packages/node/src/solana/api.solana.unit.spec.ts
  • packages/node/src/solana/block.solana.spec.ts
  • packages/node/src/solana/block.solana.ts

export type SolanaSafeApi = undefined;

const REQUEST_TIMEOUT = 30_000;
const SKIPPED_SLOT_ERROR = 'was skipped';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== target file: packages/node/src/solana/api.solana.ts ==\n'
wc -l packages/node/src/solana/api.solana.ts
sed -n '1,340p' packages/node/src/solana/api.solana.ts

printf '\n== search for skip-slot and solana error helpers ==\n'
rg -n "was skipped|SKIPPED_SLOT_ERROR|isSolanaError|SolanaError|context\.statusCode|context|code\)" packages/node/src packages -g '!**/dist/**' -g '!**/build/**' || true

Repository: subquery/subql-solana

Length of output: 9473


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== unit spec ==\n'
wc -l packages/node/src/solana/api.solana.unit.spec.ts
sed -n '1,120p' packages/node/src/solana/api.solana.unit.spec.ts

printf '\n== dependency versions ==\n'
rg -n '"`@solana/`(kit|rpc|rpc-api|addresses)"|solana' package.json packages/*/package.json packages/*/*/package.json -g 'package.json' || true

printf '\n== lockfile/package manager hints ==\n'
rg -n '"`@solana/kit`"|`@solana/rpc`|`@solana/rpc-api`' package-lock.json pnpm-lock.yaml yarn.lock npm-shrinkwrap.json || true

Repository: subquery/subql-solana

Length of output: 6624


🌐 Web query:

@solana/kit SolanaError isSolanaError skip slot error code message stripped __DEV__

💡 Result:

In the context of the @solana/kit and @solana/errors libraries, the SolanaError class and the isSolanaError type guard are used to handle typed errors [1][2][3]. When interacting with the Solana JSON-RPC, you may encounter server errors related to skipped or missing slots, such as: SOLANA_ERRORJSON_RPCSERVER_ERROR_SLOT_SKIPPED [4] SOLANA_ERRORJSON_RPCSERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED [4] These errors often map to underlying JSON-RPC responses (like codes -32007 or -32009) [5][6][7] which indicate that a block was either explicitly skipped by the network due to leader/validator lag or is unavailable due to ledger data gaps [6]. Regarding your specific query components: 1. isSolanaError: This is a type guard function used to check if a caught error is a SolanaError and, optionally, to assert its specific error code [2][3]. When used with a code, it refines the TypeScript type of the error, allowing you to safely access the error's context (e.g., e.context) [2][8]. 2. SolanaError: A class that encapsulates an error's message, stack trace, and a specific numeric Solana error code [1][9]. 3. Slot skipped errors: These indicate that the requested slot does not contain block data, either because it was skipped during consensus or the data is missing from the node's local storage [6]. 4. DEV: In development environments, the @solana/errors library may include additional error context or more descriptive messages that are sometimes stripped or minimized in production builds to reduce bundle size. To handle these errors, you should use the type guard to check for specific error codes rather than string-matching the raw JSON-RPC error message [10][11]: import { isSolanaError, SOLANA_ERRORJSON_RPCSERVER_ERROR_SLOT_SKIPPED } from '@solana/kit'; try { //... rpc call } catch (e) { if (isSolanaError(e, SOLANA_ERRORJSON_RPCSERVER_ERROR_SLOT_SKIPPED)) { // Handle skipped slot specifically using e.context.serverMessage } }

Citations:


🌐 Web query:

site:docs.solana.com @solana/kit skipped slot error code isSolanaError

💡 Result:

There is no publicly documented error code specifically named skipped slot error code or isSolanaError associated with a package named @solana/kit in the provided technical documentation. It is possible that the package name or error nomenclature is non-standard, unofficial, or refers to a internal/deprecated library. In Solana development, the term skipped slot refers to a slot in the blockchain where no block was produced, which is a normal occurrence in the cluster [1]. Developers often encounter slot-related concepts when using commitment levels (such as processed, where a block may still be skipped) or via WebSocket subscriptions like slotSubscribe or slotsUpdatesSubscribe [1][2]. If you are encountering an error with a specific library, ensure you are using the official Solana Web3.js SDK or standard RPC methods, as these are the primary interfaces for interacting with the Solana network [1][2]. If isSolanaError is part of a custom error-handling utility in your project, it is likely a helper function designed to identify error types conforming to a specific Solana error interface.

Citations:


🌐 Web query:

@solana/kit SolanaError isSolanaError skipped slot error code message stripped __DEV__

💡 Result:

In the @solana/kit ecosystem, SolanaError is a structured error class used to standardize error handling across the library [1][2]. Key components and usage: 1. Error Codes and Identification: Solana errors are identified by specific, unique error codes (e.g., SOLANA_ERRORJSON_RPCSERVER_ERROR_SLOT_SKIPPED) [3][1]. You should never rely on string matching to identify errors; instead, use the isSolanaError helper function, which acts as a TypeScript type guard [4][5]. 2. Handling Skipped Slots: A "skipped slot" occurs when a slot does not produce a block, typically because the leader was offline or the fork was abandoned [6]. When this happens, an RPC node may return a JSON-RPC error. - In @solana/kit, this is represented by the error code SOLANA_ERRORJSON_RPCSERVER_ERROR_SLOT_SKIPPED [3]. - This error code corresponds to the underlying JSON-RPC error code -32007 [7][8]. - When using isSolanaError(error, SOLANA_ERRORJSON_RPCSERVER_ERROR_SLOT_SKIPPED), the error context provides access to the serverMessage returned by the node [3]. 3. DEV and Message Stripping: In production-optimized builds, error messages are often "stripped" or replaced with generic placeholders to reduce bundle size and protect against information leakage [3][2]. The DEV environment variable is typically used by library internals to determine whether to include detailed, developer-friendly error messages or minimal, production-safe messages in the SolanaError object [2]. Example pattern for catching these errors: import { isSolanaError, SOLANA_ERRORJSON_RPCSERVER_ERROR_SLOT_SKIPPED } from '@solana/kit'; try { //... operation that might trigger a slot error } catch (e) { if (isSolanaError(e, SOLANA_ERRORJSON_RPCSERVER_ERROR_SLOT_SKIPPED)) { console.error('The requested slot was skipped:', e.context.serverMessage); } else { throw e; } }

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== yarn lock entries for `@solana/errors` and `@solana/kit` ==\n'
rg -n '"`@solana/errors`@|`@solana/errors`"| "`@solana/kit`@|`@solana/kit`"' yarn.lock || true

printf '\n== package manifests mentioning `@solana/errors` or `@solana/kit` ==\n'
rg -n '"`@solana/errors`"| "`@solana/kit`"' packages/node/package.json packages/types/package.json package.json yarn.lock || true

Repository: subquery/subql-solana

Length of output: 3158


Use Solana’s typed skipped-slot error instead of matching the message string. e.message?.includes('was skipped') is brittle; @solana/kit/@solana/errors already expose isSolanaError(...) and a dedicated skipped-slot code, so this should use the structured error path to avoid a silent regression if the text changes or is stripped.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/node/src/solana/api.solana.ts` at line 31, Replace the brittle
skipped-slot string check with Solana’s structured error handling in
api.solana.ts. Update the logic around the skipped-slot handling to use the
typed error path from `@solana/kit/`@solana/errors, using isSolanaError(...) and
the dedicated skipped-slot error code instead of checking
e.message.includes('was skipped'). Keep the SKIPPED_SLOT_ERROR symbol only if it
is still needed for the structured comparison, and make the retry/skip decision
based on the typed error fields.

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.

1 participant