Skip to content

Treat LONG_TERM_STORAGE_SLOT_SKIPPED as a skipped slot by default#32

Merged
ianhe8x merged 3 commits into
mainfrom
skip-block-2
Jun 17, 2026
Merged

Treat LONG_TERM_STORAGE_SLOT_SKIPPED as a skipped slot by default#32
ianhe8x merged 3 commits into
mainfrom
skip-block-2

Conversation

@ianhe8x

@ianhe8x ianhe8x commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

On devnet, blocks that are genuinely skipped can surface as SOLANA_ERROR__JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED (-32009, "Slot X was skipped, or missing in long-term storage") instead of the expected SLOT_SKIPPED (-32007), once the slot falls outside the RPC node's local blockstore retention window and its archival/bigtable lookup returns not-found. This code was previously left unconverted (intentionally, since it's ambiguous on mainnet — it can also mean a genuine archival gap), which still crashed the node via exitWithError whenever it occurred.

  • api.solana.ts's isSkippedSlotError now also matches -32009 and converts it to BlockUnavailableError (same handling as a confirmed skip).
  • This is opt-out via a --treatLongTermStorageSkipAsSkipped startup flag (default true, see yargs.ts), read through a new SolanaNodeConfig wrapper (configure/NodeConfig.ts) following the same pattern used for custom node options in sibling @subql node packages. It's a startup-level setting rather than per-endpoint manifest config, since it's about how an RPC response is interpreted (not specific to any one endpoint), and applies uniformly across every endpoint in the connection pool.
  • Added api.solana.skip-slot.spec.ts covering both error codes and the opt-out flag.

Test plan

  • New unit tests in api.solana.skip-slot.spec.ts pass
  • yarn workspace @subql/node-solana build succeeds
  • yarn workspace @subql/types-solana build succeeds

Summary by CodeRabbit

  • New Features

    • Added configuration option treatLongTermStorageSkipAsSkipped to control how Solana long-term storage "slot skipped" errors (RPC -32009) are handled.
    • New CLI flag to enable/disable treating these errors as unavailable slots (defaults to enabled).
  • Tests

    • Added comprehensive test suite for skipped-slot error handling scenarios.

Some RPC providers report a skipped slot as SOLANA_ERROR__JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED
(-32009) instead of SLOT_SKIPPED (-32007) once it falls outside their local blockstore
retention window, which was still crashing the node. Add a treatLongTermStorageSkipAsSkipped
endpoint config option (default true) so this is treated the same as a confirmed skip, while
allowing it to be disabled if a genuine archival data gap needs to surface as an error.
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9b4872db-784b-43f8-9ab9-dffe03e61a97

📥 Commits

Reviewing files that changed from the base of the PR and between 3a85425 and 32c7c45.

📒 Files selected for processing (7)
  • .gitignore
  • packages/node/src/configure/NodeConfig.ts
  • packages/node/src/solana/api.connection.ts
  • packages/node/src/solana/api.service.solana.ts
  • packages/node/src/solana/api.solana.skip-slot.spec.ts
  • packages/node/src/solana/api.solana.ts
  • packages/node/src/yargs.ts

📝 Walkthrough

Walkthrough

Adds a configurable treatLongTermStorageSkipAsSkipped boolean option (defaulting to true) that controls whether Solana's LONG_TERM_STORAGE_SLOT_SKIPPED RPC error (-32009) is converted to BlockUnavailableError. The flag is exposed as a CLI option, flows through ISolanaConfig/SolanaNodeConfig, SolanaApiService, SolanaApiConnection, and into a refactored isSkippedSlotError predicate inside SolanaApi.

Changes

Configurable Long-Term Storage Skip Handling

Layer / File(s) Summary
Config contract and CLI entry point
packages/node/src/configure/NodeConfig.ts, packages/node/src/yargs.ts
ISolanaConfig becomes an interface with an explicit treatLongTermStorageSkipAsSkipped: boolean field; SolanaNodeConfig exposes a getter defaulting to true; a matching CLI boolean option is registered in yargsOptions.
SolanaApi predicate and constructor wiring
packages/node/src/solana/api.solana.ts
Imports SOLANA_ERROR__JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED; replaces fixed isSkippedSlotError(e) with a parameterized version; adds #treatLongTermStorageSkipAsSkipped private field; extends constructor and SolanaApi.create signatures; updates both getHeaderByHeight and fetchBlock call sites.
Connection and service wiring
packages/node/src/solana/api.connection.ts, packages/node/src/solana/api.service.solana.ts
SolanaApiConnection.create gains an optional treatLongTermStorageSkipAsSkipped parameter forwarded to SolanaApi.create; SolanaApiService.init derives the flag from SolanaNodeConfig and passes it into SolanaApiConnection.create.
Skip-slot tests
packages/node/src/solana/api.solana.skip-slot.spec.ts
New spec mocks createSolanaRpc to inject controlled SolanaError codes and asserts the three behavioral cases: SLOT_SKIPPEDBlockUnavailableError, LONG_TERM_STORAGE_SLOT_SKIPPED (default) → BlockUnavailableError, LONG_TERM_STORAGE_SLOT_SKIPPED (flag disabled) → raw SolanaError.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

Possibly related PRs

  • subquery/subql-solana#30: Directly overlaps in the skipped-slot block-fetch path — both PRs implement or extend the mapping of Solana skipped-slot RPC failures to BlockUnavailableError in SolanaApi.

Poem

🐇 A skip is a skip, or is it? Now you choose!
Long-term storage errors need not bring the blues.
A flag set to true and the block goes away,
Set it to false and the raw error holds sway.
Hop along, indexer — no crashes today! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: adding support to treat LONG_TERM_STORAGE_SLOT_SKIPPED errors as skipped slots by default, which is the core feature of this PR.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch skip-block-2

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.


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

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Coverage report

Caution

Test run failed

St.
Category Percentage Covered / Total
🔴 Statements 56.74% 2503/4411
🟡 Branches 69.57% 256/368
🔴 Functions 50.96% 106/208
🔴 Lines 56.74% 2503/4411

Test suite run failed

Failed tests: 4/60. Failed suites: 1/9.
  ● test solana project.yaml › could get options in template from its deployment

    expect(received).toContain(expected) // indexOf

    Expected substring: "abi: Pool"
    Received string:    "dataSources:
      - kind: solana/Runtime
        mapping:
          file: ./dist/index.js
          handlers:
            - filter: {}
              handler: handlePoolCreated
              kind: solana/LogHandler
        startBlock: 12369621
      - kind: solana/Runtime
        mapping:
          file: ./dist/index.js
          handlers:
            - filter: {}
              handler: handleIncreaseLiquidity
              kind: solana/LogHandler
            - filter: {}
              handler: handleDecreaseLiquidity
              kind: solana/LogHandler
            - filter: {}
              handler: handleCollect
              kind: solana/LogHandler
            - filter: {}
              handler: handleTransfer
              kind: solana/LogHandler
        startBlock: 12369651
    network:
      chainId: '1'
    runner:
      node:
        name: '@subql/node-solana'
        version: '*'
      query:
        name: '@subql/query'
        version: '*'
    schema:
      file: ./schema.graphql
    specVersion: 1.0.0
    templates:
      - kind: solana/Runtime
        mapping:
          file: ./dist/index.js
          handlers:
            - filter: {}
              handler: handleInitialize
              kind: solana/LogHandler
            - filter: {}
              handler: handleSwap
              kind: solana/LogHandler
            - filter: {}
              handler: handleMint
              kind: solana/LogHandler
            - filter: {}
              handler: handleBurn
              kind: solana/LogHandler
            - filter: {}
              handler: handleFlash
              kind: solana/LogHandler
        name: Pool
    "

      41 |     const manifest = loadSolanaProjectManifest(path.join(projectsDir, 'project_1.0.0.yaml'));
      42 |     const deployment = manifest.toDeployment();
    > 43 |     expect(deployment).toContain('abi: Pool');
         |                        ^
      44 |   });
      45 | });
      46 |

      at Object.<anonymous> (packages/common-solana/src/project/project.spec.ts:43:24)

  ● project.yaml › can validate project.yaml

    Not implemented

      48 |   it('can validate project.yaml', () => {
      49 |     // TODO this should catch a specific error, the file doesn't currently exist
    > 50 |     throw new Error('Not implemented');
         |           ^
      51 |     expect(() => loadSolanaProjectManifest(path.join(projectsDir, 'project_falsy.yaml'))).toThrow();
      52 |     expect(() => loadSolanaProjectManifest(path.join(projectsDir, 'project_falsy_array.yaml'))).toThrow();
      53 |   });

      at Object.<anonymous> (packages/common-solana/src/project/project.spec.ts:50:11)

  ● project.yaml › can fail validation if version not supported

    Not implemented

      55 |   it('can fail validation if version not supported', () => {
      56 |     // TODO this should catch a specific error, the file doesn't currently exist
    > 57 |     throw new Error('Not implemented');
         |           ^
      58 |     expect(() => loadSolanaProjectManifest(path.join(projectsDir, 'project_invalid_version.yaml'))).toThrow('');
      59 |   });
      60 |

      at Object.<anonymous> (packages/common-solana/src/project/project.spec.ts:57:11)

  ● project.yaml › get v1.0.0 deployment mapping filter

    expect(received).toContain(expected) // indexOf

    Expected substring: "Transfer (address from, address to, uint256 tokenId)"
    Received string:    "dataSources:
      - kind: solana/Runtime
        mapping:
          file: ./dist/index.js
          handlers:
            - filter: {}
              handler: handlePoolCreated
              kind: solana/LogHandler
        startBlock: 12369621
      - kind: solana/Runtime
        mapping:
          file: ./dist/index.js
          handlers:
            - filter: {}
              handler: handleIncreaseLiquidity
              kind: solana/LogHandler
            - filter: {}
              handler: handleDecreaseLiquidity
              kind: solana/LogHandler
            - filter: {}
              handler: handleCollect
              kind: solana/LogHandler
            - filter: {}
              handler: handleTransfer
              kind: solana/LogHandler
        startBlock: 12369651
    network:
      chainId: '1'
    runner:
      node:
        name: '@subql/node-solana'
        version: '*'
      query:
        name: '@subql/query'
        version: '*'
    schema:
      file: ./schema.graphql
    specVersion: 1.0.0
    templates:
      - kind: solana/Runtime
        mapping:
          file: ./dist/index.js
          handlers:
            - filter: {}
              handler: handleInitialize
              kind: solana/LogHandler
            - filter: {}
              handler: handleSwap
              kind: solana/LogHandler
            - filter: {}
              handler: handleMint
              kind: solana/LogHandler
            - filter: {}
              handler: handleBurn
              kind: solana/LogHandler
            - filter: {}
              handler: handleFlash
              kind: solana/LogHandler
        name: Pool
    "

      70 |     const deploymentString = manifestVersioned.toDeployment();
      71 |     expect(filter).not.toBeNull();
    > 72 |     expect(deploymentString).toContain('Transfer (address from, address to, uint256 tokenId)');
         |                              ^
      73 |   });
      74 |
      75 |   it('can convert genesis hash in v1.0.0 to chainId in deployment', () => {

      at Object.<anonymous> (packages/common-solana/src/project/project.spec.ts:72:30)

Report generated by 🧪jest coverage report action from 32c7c45

@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: 3a85425ace

ℹ️ 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 packages/types/src/project.ts Outdated
* crash the node so a genuine gap in archival data isn't silently skipped.
* @default true
*/
treatLongTermStorageSkipAsSkipped?: boolean;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add the endpoint validator for the opt-out flag

When users set network.endpoint: { <url>: { treatLongTermStorageSkipAsSkipped: false } } in a project manifest, endpoint objects are validated through SolanaEndpointConfig in packages/common-solana/src/project/versioned/v1_0_0/model.ts, which currently only decorates batchSize plus inherited common fields. Adding this property only to the interface means the manifest validator will not accept/preserve the new key, so the advertised opt-out cannot be used from normal endpoint config and SolanaApi.create will keep defaulting to true.

Useful? React with 👍 / 👎.

ianhe8x added 2 commits June 18, 2026 11:22
This is about how an RPC response is interpreted, not something tied to a specific
endpoint, so it doesn't belong in the per-endpoint manifest config (where it would
also raise the question of mixing enabled/disabled per endpoint in the same pool).
Expose it instead as a --treat-long-term-storage-skip-as-skip startup flag (default
true), read via a SolanaNodeConfig wrapper following the same pattern used for
custom node options in other @subql node packages, and apply it uniformly across
every endpoint connection.
@ianhe8x ianhe8x merged commit acca498 into main Jun 17, 2026
1 of 3 checks passed
@ianhe8x ianhe8x deleted the skip-block-2 branch June 17, 2026 23:42
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