Skip to content

fix: handle non-sequential unfinalized blocks with backfill#3016

Merged
ianhe8x merged 6 commits intomainfrom
unfinalized-non-sequentential
Feb 26, 2026
Merged

fix: handle non-sequential unfinalized blocks with backfill#3016
ianhe8x merged 6 commits intomainfrom
unfinalized-non-sequentential

Conversation

@ianhe8x
Copy link
Collaborator

@ianhe8x ianhe8x commented Feb 26, 2026

Description

Previously, when dictionary returned non-sequential blocks that exceeded the finalized height,
the the indexer would crash with error "Unfinalized block is not sequential".

This fix:

  • Uses UNFINALIZED_THRESHOLD (200) to determine if gap is too large to backfill
  • If gap is within threshold, backfill missing blocks and validate parentHash chain
    • Added parentHash validation for sequential blocks to detect forks early
    • Added new test cases for backfill functionality

Fixes # (issue)

Type of change

Please delete options that are not relevant.

  • 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

  • New Features

    • Automatic backfilling of missing blocks to maintain continuous unfinalized chain connectivity.
    • Event-driven best-height updates to improve synchronization responsiveness.
  • Bug Fixes

    • Enhanced fork detection and recovery during backfill and after new blocks arrive.
    • Safer handling of out-of-window/unfinalized blocks and clearer error propagation on fetch failures.
  • Tests

    • Expanded test coverage for backfill, gap, fork, continuity, and error scenarios.
  • Style

    • Formatting and import/object-literal tidy-ups across tests and helpers.

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

Adds backfill and fork-detection logic to UnfinalizedBlocksService, integrates best-height tracking via events, changes registerUnfinalizedBlock to return a fork Header when detected, and expands tests to cover many backfill/fork scenarios and error paths.

Changes

Cohort / File(s) Summary
Backfill Implementation
packages/node-core/src/indexer/unfinalizedBlocks.service.ts
Adds private backfillBlocks(startHeight, endHeight, nextHeader) to fetch/validate missing headers, detects forks during backfill, updates registerUnfinalizedBlock() signature to return Promise<Header | undefined>, introduces _latestBestHeight/bestHeight tracking and updateBestHeight() handler, applies safe-height pruning, adds logging and a runtime assertion in getLastCorrectFinalizedBlock().
Backfill Test Suite
packages/node-core/src/indexer/unfinalizedBlocks.service.spec.ts
Expands tests with many backfill scenarios (small/large gaps, threshold resets, fork detection during/after backfill, fetch errors, continuity checks). Adds mockBestHeight, extends mocks/helpers (getMockMetadata, mockStoreCache, mockBlock) and adjusts formatting/shorthand in tests.

Sequence Diagram(s)

sequenceDiagram
  participant Unfinalized as UnfinalizedBlocksService
  participant Chain as BlockchainService
  participant Store as Store/Cache
  participant Event as IndexerEventBus

  Unfinalized->>Unfinalized: updateBestHeight({height})
  Unfinalized->>Unfinalized: registerUnfinalizedBlock(header)
  alt header non-sequential (gap)
    Unfinalized->>Chain: getHeaderForHeight(range)  (backfill)
    Chain-->>Unfinalized: headers / error
    alt fetch error
      Unfinalized->>Unfinalized: propagate error
    else headers returned
      Unfinalized->>Unfinalized: validate parentHash chain
      alt fork detected
        Unfinalized->>Event: emit ForkDetected(forkHeader)
        Unfinalized->>Store: prepare reset / drop below safeHeight
        Unfinalized-->>Unfinalized: return forkHeader
      else backfill success
        Unfinalized->>Store: append backfilled headers
        Unfinalized->>Store: append triggering header
        Unfinalized->>Event: emit BlockRegistered(header)
      end
    end
  else sequential
    Unfinalized->>Store: append header
    Unfinalized->>Event: emit BlockRegistered(header)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐇 I hop through gaps and fetch each bead,
Tying parents where the chain might need,
If forks pop up I thump my drum,
Backfilled paths now hum-hum-hum,
Hooray — the blocks are snug indeed!

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'fix: handle non-sequential unfinalized blocks with backfill' directly describes the main change: introducing backfill functionality to handle non-sequential unfinalized blocks, which is the core objective and primary change across both modified files.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch unfinalized-non-sequentential

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

Copy link

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

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: 41c60ddf6f

ℹ️ 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 on lines 139 to 140
if (lastUnfinalized && lastUnfinalized.blockHash !== header.parentHash) {
return header;

Choose a reason for hiding this comment

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

P1 Badge Skip fork detection when parent hash is unavailable

This new sequential-path check flags a fork whenever header.parentHash differs from the previous hash, but Header.parentHash is allowed to be undefined and some callers/tests pass empty values, so valid sequential blocks are now treated as forks after the first block. In those environments the indexer will repeatedly return rewind targets instead of progressing. Only perform this fork check when the new header actually has a parent hash to validate.

Useful? React with 👍 / 👎.

Comment on lines +318 to +320
assert(
bestHeader.blockHeight === checkingHeader.blockHeight,
'Expect best header and checking header to be at the same height'

Choose a reason for hiding this comment

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

P1 Badge Remove height-equality assert in rewind traversal

The new assertion assumes checkingHeader and the first bestVerifiableBlocks entry always share a height, but after early fork detection in registerUnfinalizedBlock we can enter getLastCorrectFinalizedBlock with a header above finalized height (for example, latest unfinalized block has a bad parent while finalized has advanced), so this assertion throws and crashes instead of finding a valid rewind point. The previous parent-walk logic handled this case and should not be blocked by a strict height equality check.

Useful? React with 👍 / 👎.

Copy link

@coderabbitai coderabbitai bot left a comment

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 (3)
packages/node-core/src/indexer/unfinalizedBlocks.service.spec.ts (1)

389-394: Minor: clarify the comment about block count.

The comment says // 111 + 139 backfilled + 250 but it's actually 1 (block 111) + 138 (blocks 112-249) + 1 (block 250) = 140. The math works out, but the "139 backfilled" is off by one. Consider updating for clarity.

📝 Proposed fix
-      expect(unfinalizedBlocks.length).toBe(140); // 111 + 139 backfilled + 250
+      expect(unfinalizedBlocks.length).toBe(140); // block 111 + 138 backfilled (112-249) + block 250
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/node-core/src/indexer/unfinalizedBlocks.service.spec.ts` around
lines 389 - 394, The comment on the assertion for unfinalizedBlocks length is
misleading: update the inline comment next to the
expect(unfinalizedBlocks.length).toBe(140) to correctly describe the components
as "1 (block 111) + 138 (blocks 112-249) + 1 (block 250) = 140" so it matches
the actual ranges; reference the test's unfinalizedBlocks variable in
unfinalizedBlocks.service.spec.ts when making this change.
packages/node-core/src/indexer/unfinalizedBlocks.service.ts (2)

173-180: Consider adding a clarifying comment about the intentional push before fork check.

The header is pushed to unfinalizedBlocks (line 174) before checking if backfill detected a fork. If a fork was detected, the blocks aren't saved (line 179 is skipped), leaving an inconsistent in-memory state. This works because the caller will trigger a reindex that resets the blocks, but the intent isn't immediately obvious.

📝 Proposed comment for clarity
     // Add the original header after successful backfill
+    // Note: We push even if fork was detected. The in-memory state will be reset
+    // when the caller processes the fork and triggers a reindex.
     this.unfinalizedBlocks.push(header);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/node-core/src/indexer/unfinalizedBlocks.service.ts` around lines 173
- 180, The code pushes the new header into this.unfinalizedBlocks before
checking backfillResult.forkDetected, leaving in-memory state modified even when
a fork is detected and saveUnfinalizedBlocks is skipped; add a clarifying inline
comment above the this.unfinalizedBlocks.push(header) (or move the push if you
prefer) stating that the push is intentional to preserve the original header for
caller-triggered reindex/reset on fork detection, and reference
backfillResult.forkDetected and saveUnfinalizedBlocks in the comment so future
readers understand why the in-memory list is left unchanged on fork and that
persistence only occurs when no fork is detected.

216-219: Use unknown instead of any for caught errors.

TypeScript best practice is to use unknown for caught exceptions and perform type narrowing.

♻️ Proposed fix
-      } catch (e: any) {
-        logger.error(`Failed to fetch block ${height} during backfill: ${e.message}`);
-        throw new Error(`Failed to backfill missing unfinalized block at height ${height}: ${e.message}`);
+      } catch (e: unknown) {
+        const message = e instanceof Error ? e.message : String(e);
+        logger.error(`Failed to fetch block ${height} during backfill: ${message}`);
+        throw new Error(`Failed to backfill missing unfinalized block at height ${height}: ${message}`);
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/node-core/src/indexer/unfinalizedBlocks.service.ts` around lines 216
- 219, In the catch block in unfinalizedBlocks.service.ts (the try/catch that
logs and rethrows failed backfill for a given height), change the catch
parameter from e: any to e: unknown and narrow it before accessing message;
e.g., determine a message via `const msg = e instanceof Error ? e.message :
String(e)` and then use msg in both logger.error and the thrown Error to avoid
unsafe any usage while preserving the original error text.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/node-core/src/indexer/unfinalizedBlocks.service.ts`:
- Around line 318-321: The assertion in getLastCorrectFinalizedBlock wrongly
assumes bestHeader.blockHeight === checkingHeader.blockHeight; remove that
assert and instead align heights by walking the taller header backward until
heights match (use parentHash lookups in unfinalizedBlocks or the same
height-walking logic from hasForked), then compare headers; ensure you reference
forkedHeader/checkingHeader, bestVerifiableBlocks and unfinalizedBlocks when
performing the backward-walk and final parentHash comparisons so the function
handles cases where finalizedBlockNumber advanced and unfinalizedBlocks contain
mixed heights.

---

Nitpick comments:
In `@packages/node-core/src/indexer/unfinalizedBlocks.service.spec.ts`:
- Around line 389-394: The comment on the assertion for unfinalizedBlocks length
is misleading: update the inline comment next to the
expect(unfinalizedBlocks.length).toBe(140) to correctly describe the components
as "1 (block 111) + 138 (blocks 112-249) + 1 (block 250) = 140" so it matches
the actual ranges; reference the test's unfinalizedBlocks variable in
unfinalizedBlocks.service.spec.ts when making this change.

In `@packages/node-core/src/indexer/unfinalizedBlocks.service.ts`:
- Around line 173-180: The code pushes the new header into
this.unfinalizedBlocks before checking backfillResult.forkDetected, leaving
in-memory state modified even when a fork is detected and saveUnfinalizedBlocks
is skipped; add a clarifying inline comment above the
this.unfinalizedBlocks.push(header) (or move the push if you prefer) stating
that the push is intentional to preserve the original header for
caller-triggered reindex/reset on fork detection, and reference
backfillResult.forkDetected and saveUnfinalizedBlocks in the comment so future
readers understand why the in-memory list is left unchanged on fork and that
persistence only occurs when no fork is detected.
- Around line 216-219: In the catch block in unfinalizedBlocks.service.ts (the
try/catch that logs and rethrows failed backfill for a given height), change the
catch parameter from e: any to e: unknown and narrow it before accessing
message; e.g., determine a message via `const msg = e instanceof Error ?
e.message : String(e)` and then use msg in both logger.error and the thrown
Error to avoid unsafe any usage while preserving the original error text.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1fbbccc and 41c60dd.

📒 Files selected for processing (2)
  • packages/node-core/src/indexer/unfinalizedBlocks.service.spec.ts
  • packages/node-core/src/indexer/unfinalizedBlocks.service.ts

Comment on lines +318 to +321
assert(
bestHeader.blockHeight === checkingHeader.blockHeight,
'Expect best header and checking header to be at the same height'
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, get the file and look at the relevant lines and surrounding context
wc -l packages/node-core/src/indexer/unfinalizedBlocks.service.ts

Repository: subquery/subql

Length of output: 121


🏁 Script executed:

# Read the file to understand the getLastCorrectFinalizedBlock method
cat -n packages/node-core/src/indexer/unfinalizedBlocks.service.ts | sed -n '300,340p'

Repository: subquery/subql

Length of output: 1901


🏁 Script executed:

# Look at the entire getLastCorrectFinalizedBlock method - find its start
rg -n "getLastCorrectFinalizedBlock" packages/node-core/src/indexer/unfinalizedBlocks.service.ts -B 5 -A 30

Repository: subquery/subql

Length of output: 2974


🏁 Script executed:

# Find registerUnfinalizedBlock method and lines 140 and 177
rg -n "registerUnfinalizedBlock" packages/node-core/src/indexer/unfinalizedBlocks.service.ts -A 50 | head -100

Repository: subquery/subql

Length of output: 3361


🏁 Script executed:

# Find hasForked method to compare with assertion
rg -n "hasForked" packages/node-core/src/indexer/unfinalizedBlocks.service.ts -A 30

Repository: subquery/subql

Length of output: 2828


🏁 Script executed:

# Find where getLastCorrectFinalizedBlock is called and what is passed
rg -n "getLastCorrectFinalizedBlock\(" packages/node-core/src/indexer/unfinalizedBlocks.service.ts -B 5 -A 2

Repository: subquery/subql

Length of output: 644


🏁 Script executed:

# Check backfillBlocks and what heights it can return
rg -n "backfillBlocks" packages/node-core/src/indexer/unfinalizedBlocks.service.ts -A 40 | head -80

Repository: subquery/subql

Length of output: 2893


🏁 Script executed:

# Also check what forkHeader could be from backfillResult
rg -n "forkHeader" packages/node-core/src/indexer/unfinalizedBlocks.service.ts -B 2 -A 2

Repository: subquery/subql

Length of output: 826


🏁 Script executed:

# Check if there are any tests that exercise this scenario
fd -t f "\.test\.(ts|js)" packages/node-core | xargs grep -l "unfinalizedBlocks\|getLastCorrectFinalizedBlock" 2>/dev/null

Repository: subquery/subql

Length of output: 42


🏁 Script executed:

# Check the UNFINALIZED_THRESHOLD constant value
rg -n "UNFINALIZED_THRESHOLD" packages/node-core/src/indexer/unfinalizedBlocks.service.ts

Repository: subquery/subql

Length of output: 260


🏁 Script executed:

# Look at the complete flow: where registerUnfinalizedBlock is called and how forkedHeader is used
rg -n "processUnfinalizedBlockHeader" packages/node-core/src/indexer/unfinalizedBlocks.service.ts -B 5 -A 25 | head -60

Repository: subquery/subql

Length of output: 2445


🏁 Script executed:

# Check if there's any other code path that might adjust heights before calling getLastCorrectFinalizedBlock
rg -n "getLastCorrectFinalizedBlock" packages/node-core/src/indexer/unfinalizedBlocks.service.ts -B 10

Repository: subquery/subql

Length of output: 852


🏁 Script executed:

# Check deleteFinalizedBlock to understand when blocks <= finalizedBlockNumber remain in unfinalizedBlocks
rg -n "deleteFinalizedBlock" packages/node-core/src/indexer/unfinalizedBlocks.service.ts -A 20

Repository: subquery/subql

Length of output: 1916


🏁 Script executed:

# Also check the order of operations when finalizedBlockNumber changes
rg -n "finalizedBlockNumber\|_finalizedHeader" packages/node-core/src/indexer/unfinalizedBlocks.service.ts | head -30

Repository: subquery/subql

Length of output: 40


🏁 Script executed:

# Get the complete flow to understand when finalizedBlockNumber is set vs when getLastCorrectFinalizedBlock is called
cat -n packages/node-core/src/indexer/unfinalizedBlocks.service.ts | sed -n '94,127p'

Repository: subquery/subql

Length of output: 1345


🏁 Script executed:

# Check if registerFinalizedBlock is called before or after processUnfinalizedBlockHeader
rg -n "registerFinalizedBlock\|processUnfinalizedBlockHeader" packages/node-core/src/indexer -A 2 -B 2 --type=ts | head -100

Repository: subquery/subql

Length of output: 40


🏁 Script executed:

# Final check: Look at the overall flow and understand the exact scenario
# Get more context around processUnfinalizedBlockHeader and when it's called
cat -n packages/node-core/src/indexer/unfinalizedBlocks.service.ts | sed -n '99,250p'

Repository: subquery/subql

Length of output: 7288


🏁 Script executed:

# Verify: When registerUnfinalizedBlock returns a header, is it guaranteed to be > finalizedBlockNumber?
cat -n packages/node-core/src/indexer/unfinalizedBlocks.service.ts | sed -n '130,145p'

Repository: subquery/subql

Length of output: 875


Assertion failure in getLastCorrectFinalizedBlock when fork detected via registerUnfinalizedBlock at lines 318-321.

The assertion assumes bestHeader and forkedHeader are at the same height, but this invariant is not guaranteed:

  1. registerUnfinalizedBlock (line 131) only processes headers with blockHeight > finalizedBlockNumber and returns them as forkedHeader
  2. When finalizedBlockNumber increases, blocks in unfinalizedBlocks may become <= finalizedBlockNumber but remain undeleted (cleanup only happens on line 241 when no fork is detected)
  3. getLastCorrectFinalizedBlock receives forkedHeader (height > finalizedBlockNumber) and filters bestVerifiableBlocks to heights <= finalizedBlockNumber
  4. First loop iteration: bestHeader.blockHeight <= finalizedBlockNumber while checkingHeader.blockHeight > finalizedBlockNumberassertion always fails

Unlike hasForked() which explicitly walks backward to match heights (lines 289–291), registerUnfinalizedBlock paths provide no such guarantee.

Scenario:

  • Unfinalized blocks: [100, 101, 102, 103], finalized = 99
  • finalizedBlockNumber increases to 102
  • Block 104 arrives with wrong parentHash → returned as forkedHeader
  • bestVerifiableBlocks = [100, 101, 102] (filtered from unfinalizedBlocks)
  • Loop iteration 1: bestHeader.blockHeight = 102, checkingHeader.blockHeight = 104assertion fails
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/node-core/src/indexer/unfinalizedBlocks.service.ts` around lines 318
- 321, The assertion in getLastCorrectFinalizedBlock wrongly assumes
bestHeader.blockHeight === checkingHeader.blockHeight; remove that assert and
instead align heights by walking the taller header backward until heights match
(use parentHash lookups in unfinalizedBlocks or the same height-walking logic
from hasForked), then compare headers; ensure you reference
forkedHeader/checkingHeader, bestVerifiableBlocks and unfinalizedBlocks when
performing the backward-walk and final parentHash comparisons so the function
handles cases where finalizedBlockNumber advanced and unfinalizedBlocks contain
mixed heights.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 26, 2026

Coverage report for .

Caution

Test run failed

St.
Category Percentage Covered / Total
🟢 Statements
87.39% (+17.28% 🔼)
71001/81250
🟢 Branches
80.39% (+2.1% 🔼)
3365/4186
🟡 Functions
65.79% (+3.29% 🔼)
1258/1912
🟢 Lines
87.39% (+17.28% 🔼)
71001/81250

⚠️ Details were not displayed: the report size has exceeded the limit.

Test suite run failed

Failed tests: 20/723. Failed suites: 6/116.
  ● MultiChain Rewind Service › The situation where notifyHandle controls the state › should handle multiple concurrent rewind requests

    expect(received).toBe(expected) // Object.is equality

    Expected: "complete"
    Received: "normal"

      304 |       await tx.commit();
      305 |       // This can fail because the notification has already come in and the status is back to normal
    > 306 |       expect(multiChainRewindService2.status).toBe(MultiChainRewindStatus.Complete);
          |                                               ^
      307 |
      308 |       await delay(notifyHandleDelay);
      309 |       // The last chain rollback is complete, all chains have finished rolling back.

      at Object.<anonymous> (packages/node-core/src/indexer/multiChainRewind.service.test.ts:306:47)


  ● BaseProjectService › initializing services › succeeds with an unfinalized blocks rewind

    expect(received).resolves.not.toThrow()

    Received promise rejected instead of resolved
    Rejected to value: [AssertionError: Expect best header and checking header to be at the same height]

      483 |       const reindexSpy = jest.spyOn(service, 'reindex');
      484 |
    > 485 |       await expect(service.init()).resolves.not.toThrow();
          |             ^
      486 |
      487 |       expect(reindexSpy).toHaveReturnedTimes(1);
      488 |     });

      at expect (node_modules/expect/build/index.js:105:15)
      at Object.<anonymous> (packages/node-core/src/indexer/project.service.spec.ts:485:13)


  ● Dictionary V1 › coreDictionary › set startHeight of this dictionary

    ApolloError: Response not successful: Received status code 500

      at new ApolloError (packages/node-core/node_modules/@apollo/client/errors/index.js:59:28)
      at packages/node-core/node_modules/@apollo/client/core/QueryManager.js:823:71
      at both (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:35:31)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:72
      at Object.then (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:24)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:37:49)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:68
          at Array.forEach (<anonymous>)
      at iterateObserversSafely (packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:25)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/Concast.js:76:21)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at handleError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:170:14)
      at packages/node-core/node_modules/@apollo/client/link/http/createHttpLink.js:148:17

    Cause:
    ServerError: Response not successful: Received status code 500

      at Object.throwServerError (packages/node-core/node_modules/@apollo/client/link/utils/throwServerError.js:19:17)
      at throwServerError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:120:9)
      at packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:176:48

  ● Dictionary V1 › coreDictionary › validateChainMeta and useDictionary

    ApolloError: Response not successful: Received status code 500

      at new ApolloError (packages/node-core/node_modules/@apollo/client/errors/index.js:59:28)
      at packages/node-core/node_modules/@apollo/client/core/QueryManager.js:823:71
      at both (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:35:31)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:72
      at Object.then (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:24)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:37:49)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:68
          at Array.forEach (<anonymous>)
      at iterateObserversSafely (packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:25)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/Concast.js:76:21)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at handleError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:170:14)
      at packages/node-core/node_modules/@apollo/client/link/http/createHttpLink.js:148:17

    Cause:
    ServerError: Response not successful: Received status code 500

      at Object.throwServerError (packages/node-core/node_modules/@apollo/client/link/utils/throwServerError.js:19:17)
      at throwServerError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:120:9)
      at packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:176:48

  ● Dictionary V1 › coreDictionary › validate dictionary with a height

    ApolloError: Response not successful: Received status code 500

      at new ApolloError (packages/node-core/node_modules/@apollo/client/errors/index.js:59:28)
      at packages/node-core/node_modules/@apollo/client/core/QueryManager.js:823:71
      at both (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:35:31)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:72
      at Object.then (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:24)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:37:49)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:68
          at Array.forEach (<anonymous>)
      at iterateObserversSafely (packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:25)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/Concast.js:76:21)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at handleError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:170:14)
      at packages/node-core/node_modules/@apollo/client/link/http/createHttpLink.js:148:17

    Cause:
    ServerError: Response not successful: Received status code 500

      at Object.throwServerError (packages/node-core/node_modules/@apollo/client/link/utils/throwServerError.js:19:17)
      at throwServerError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:120:9)
      at packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:176:48

  ● Dictionary V1 › coreDictionary › able to build queryEntryMap

    ApolloError: Response not successful: Received status code 500

      at new ApolloError (packages/node-core/node_modules/@apollo/client/errors/index.js:59:28)
      at packages/node-core/node_modules/@apollo/client/core/QueryManager.js:823:71
      at both (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:35:31)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:72
      at Object.then (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:24)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:37:49)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:68
          at Array.forEach (<anonymous>)
      at iterateObserversSafely (packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:25)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/Concast.js:76:21)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at handleError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:170:14)
      at packages/node-core/node_modules/@apollo/client/link/http/createHttpLink.js:148:17

    Cause:
    ServerError: Response not successful: Received status code 500

      at Object.throwServerError (packages/node-core/node_modules/@apollo/client/link/utils/throwServerError.js:19:17)
      at throwServerError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:120:9)
      at packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:176:48

  ● Dictionary V1 › coreDictionary › can use scoped dictionary query

    ApolloError: Response not successful: Received status code 500

      at new ApolloError (packages/node-core/node_modules/@apollo/client/errors/index.js:59:28)
      at packages/node-core/node_modules/@apollo/client/core/QueryManager.js:823:71
      at both (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:35:31)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:72
      at Object.then (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:24)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:37:49)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:68
          at Array.forEach (<anonymous>)
      at iterateObserversSafely (packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:25)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/Concast.js:76:21)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at handleError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:170:14)
      at packages/node-core/node_modules/@apollo/client/link/http/createHttpLink.js:148:17

    Cause:
    ServerError: Response not successful: Received status code 500

      at Object.throwServerError (packages/node-core/node_modules/@apollo/client/link/utils/throwServerError.js:19:17)
      at throwServerError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:120:9)
      at packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:176:48

  ● Dictionary V1 › coreDictionary › able to getDicitonaryQueryEntries

    ApolloError: Response not successful: Received status code 500

      at new ApolloError (packages/node-core/node_modules/@apollo/client/errors/index.js:59:28)
      at packages/node-core/node_modules/@apollo/client/core/QueryManager.js:823:71
      at both (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:35:31)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:72
      at Object.then (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:24)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:37:49)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:68
          at Array.forEach (<anonymous>)
      at iterateObserversSafely (packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:25)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/Concast.js:76:21)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at handleError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:170:14)
      at packages/node-core/node_modules/@apollo/client/link/http/createHttpLink.js:148:17

    Cause:
    ServerError: Response not successful: Received status code 500

      at Object.throwServerError (packages/node-core/node_modules/@apollo/client/link/utils/throwServerError.js:19:17)
      at throwServerError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:120:9)
      at packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:176:48

  ● Dictionary V1 › get metadata

    ApolloError: Response not successful: Received status code 500

      at new ApolloError (packages/node-core/node_modules/@apollo/client/errors/index.js:59:28)
      at packages/node-core/node_modules/@apollo/client/core/QueryManager.js:823:71
      at both (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:35:31)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:72
      at Object.then (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:24)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:37:49)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:68
          at Array.forEach (<anonymous>)
      at iterateObserversSafely (packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:25)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/Concast.js:76:21)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at handleError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:170:14)
      at packages/node-core/node_modules/@apollo/client/link/http/createHttpLink.js:148:17

    Cause:
    ServerError: Response not successful: Received status code 500

      at Object.throwServerError (packages/node-core/node_modules/@apollo/client/link/utils/throwServerError.js:19:17)
      at throwServerError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:120:9)
      at packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:176:48

  ● Dictionary V1 › init metadata and get metadata

    ApolloError: Response not successful: Received status code 500

      at new ApolloError (packages/node-core/node_modules/@apollo/client/errors/index.js:59:28)
      at packages/node-core/node_modules/@apollo/client/core/QueryManager.js:823:71
      at both (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:35:31)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:72
      at Object.then (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:24)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:37:49)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:68
          at Array.forEach (<anonymous>)
      at iterateObserversSafely (packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:25)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/Concast.js:76:21)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at handleError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:170:14)
      at packages/node-core/node_modules/@apollo/client/link/http/createHttpLink.js:148:17

    Cause:
    ServerError: Response not successful: Received status code 500

      at Object.throwServerError (packages/node-core/node_modules/@apollo/client/link/utils/throwServerError.js:19:17)
      at throwServerError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:120:9)
      at packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:176:48

  ● Dictionary V1 › return dictionary query result

    ApolloError: Response not successful: Received status code 500

      at new ApolloError (packages/node-core/node_modules/@apollo/client/errors/index.js:59:28)
      at packages/node-core/node_modules/@apollo/client/core/QueryManager.js:823:71
      at both (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:35:31)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:72
      at Object.then (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:24)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:37:49)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:68
          at Array.forEach (<anonymous>)
      at iterateObserversSafely (packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:25)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/Concast.js:76:21)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at handleError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:170:14)
      at packages/node-core/node_modules/@apollo/client/link/http/createHttpLink.js:148:17

    Cause:
    ServerError: Response not successful: Received status code 500

      at Object.throwServerError (packages/node-core/node_modules/@apollo/client/link/utils/throwServerError.js:19:17)
      at throwServerError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:120:9)
      at packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:176:48

  ● Dictionary V1 › should return undefined startblock height greater than dictionary last processed height

    ApolloError: Response not successful: Received status code 500

      at new ApolloError (packages/node-core/node_modules/@apollo/client/errors/index.js:59:28)
      at packages/node-core/node_modules/@apollo/client/core/QueryManager.js:823:71
      at both (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:35:31)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:72
      at Object.then (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:24)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:37:49)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:68
          at Array.forEach (<anonymous>)
      at iterateObserversSafely (packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:25)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/Concast.js:76:21)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at handleError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:170:14)
      at packages/node-core/node_modules/@apollo/client/link/http/createHttpLink.js:148:17

    Cause:
    ServerError: Response not successful: Received status code 500

      at Object.throwServerError (packages/node-core/node_modules/@apollo/client/link/utils/throwServerError.js:19:17)
      at throwServerError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:120:9)
      at packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:176:48

  ● Dictionary V1 › should use metadata last process height at end of query height

    ApolloError: Response not successful: Received status code 500

      at new ApolloError (packages/node-core/node_modules/@apollo/client/errors/index.js:59:28)
      at packages/node-core/node_modules/@apollo/client/core/QueryManager.js:823:71
      at both (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:35:31)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:72
      at Object.then (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:24)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:37:49)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:68
          at Array.forEach (<anonymous>)
      at iterateObserversSafely (packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:25)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/Concast.js:76:21)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at handleError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:170:14)
      at packages/node-core/node_modules/@apollo/client/link/http/createHttpLink.js:148:17

    Cause:
    ServerError: Response not successful: Received status code 500

      at Object.throwServerError (packages/node-core/node_modules/@apollo/client/link/utils/throwServerError.js:19:17)
      at throwServerError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:120:9)
      at packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:176:48

  ● Individual dictionary V1 test › return undefined when dictionary api failed

    ApolloError: Response not successful: Received status code 500

      at new ApolloError (packages/node-core/node_modules/@apollo/client/errors/index.js:59:28)
      at packages/node-core/node_modules/@apollo/client/core/QueryManager.js:823:71
      at both (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:35:31)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:72
      at Object.then (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:24)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:37:49)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:68
          at Array.forEach (<anonymous>)
      at iterateObserversSafely (packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:25)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/Concast.js:76:21)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at handleError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:170:14)
      at packages/node-core/node_modules/@apollo/client/link/http/createHttpLink.js:148:17

    Cause:
    ServerError: Response not successful: Received status code 500

      at Object.throwServerError (packages/node-core/node_modules/@apollo/client/link/utils/throwServerError.js:19:17)
      at throwServerError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:120:9)
      at packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:176:48

  ● Individual dictionary V1 test › limits the dictionary query to that block range

    ApolloError: Response not successful: Received status code 500

      at new ApolloError (packages/node-core/node_modules/@apollo/client/errors/index.js:59:28)
      at packages/node-core/node_modules/@apollo/client/core/QueryManager.js:823:71
      at both (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:35:31)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:72
      at Object.then (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:24)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:37:49)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:68
          at Array.forEach (<anonymous>)
      at iterateObserversSafely (packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:25)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/Concast.js:76:21)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at handleError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:170:14)
      at packages/node-core/node_modules/@apollo/client/link/http/createHttpLink.js:148:17

    Cause:
    ServerError: Response not successful: Received status code 500

      at Object.throwServerError (packages/node-core/node_modules/@apollo/client/link/utils/throwServerError.js:19:17)
      at throwServerError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:120:9)
      at packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:176:48

  ● Individual dictionary V1 test › test query the correct range

    ApolloError: Response not successful: Received status code 500

      at new ApolloError (packages/node-core/node_modules/@apollo/client/errors/index.js:59:28)
      at packages/node-core/node_modules/@apollo/client/core/QueryManager.js:823:71
      at both (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:35:31)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:72
      at Object.then (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:24)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:37:49)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:68
          at Array.forEach (<anonymous>)
      at iterateObserversSafely (packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:25)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/Concast.js:76:21)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at handleError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:170:14)
      at packages/node-core/node_modules/@apollo/client/link/http/createHttpLink.js:148:17

    Cause:
    ServerError: Response not successful: Received status code 500

      at Object.throwServerError (packages/node-core/node_modules/@apollo/client/link/utils/throwServerError.js:19:17)
      at throwServerError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:120:9)
      at packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:176:48

  ● Individual dictionary V1 test › use minimum value of event/extrinsic returned block as batch end block

    ApolloError: Response not successful: Received status code 500

      at new ApolloError (packages/node-core/node_modules/@apollo/client/errors/index.js:59:28)
      at packages/node-core/node_modules/@apollo/client/core/QueryManager.js:823:71
      at both (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:35:31)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:72
      at Object.then (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:24:24)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/asyncMap.js:37:49)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:68
          at Array.forEach (<anonymous>)
      at iterateObserversSafely (packages/node-core/node_modules/@apollo/client/utilities/observables/iteration.js:11:25)
      at Object.error (packages/node-core/node_modules/@apollo/client/utilities/observables/Concast.js:76:21)
      at notifySubscription (node_modules/zen-observable/lib/Observable.js:140:18)
      at onNotify (node_modules/zen-observable/lib/Observable.js:179:3)
      at SubscriptionObserver.error (node_modules/zen-observable/lib/Observable.js:240:7)
      at handleError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:170:14)
      at packages/node-core/node_modules/@apollo/client/link/http/createHttpLink.js:148:17

    Cause:
    ServerError: Response not successful: Received status code 500

      at Object.throwServerError (packages/node-core/node_modules/@apollo/client/link/utils/throwServerError.js:19:17)
      at throwServerError (packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:120:9)
      at packages/node-core/node_modules/@apollo/client/link/http/parseAndCheckHttpResponse.js:176:48


  ● Substrate DictionaryService › should return all specVersion

    expect(received).toBeGreaterThan(expected)

    Matcher error: received value must be a number or bigint

    Received has value: undefined

      63 |     const specVersions = await dictionaryService.getSpecVersions();
      64 |
    > 65 |     expect(specVersions?.length).toBeGreaterThan(0);
         |                                  ^
      66 |     dictionaryService.onApplicationShutdown();
      67 |   }, 50000);
      68 | });

      at Object.<anonymous> (packages/node/src/indexer/dictionary/v1/substrateDictionaryV1.spec.ts:65:34)


  ● etherscan api › Deployment information › can get the deploy height of a contract

    Etherscan API error: Free API access is not supported for this chain. Please upgrade your api plan for full chain coverage. https://etherscan.io/apis

      24 |       return undefined;
      25 |     }
    > 26 |     throw new Error(`Etherscan API error: ${data.result}`);
         |           ^
      27 |   }
      28 |
      29 |   return data.result;

      at runRequest (packages/cli/src/utils/etherscan.ts:26:11)
      at fetchContractDeployHeight (packages/cli/src/utils/etherscan.ts:69:18)
      at Object.<anonymous> (packages/cli/src/utils/etherscan.spec.ts:41:22)


  ● Promise Utils › utils.promise delay()

    expect(received).toBeLessThan(expected)

    Expected: < 1050
    Received:   1164

      10 |     const millsecDiff = new Date().getTime() - start.getTime();
      11 |     expect(millsecDiff).toBeGreaterThanOrEqual(1000);
    > 12 |     expect(millsecDiff).toBeLessThan(1050);
         |                         ^
      13 |   });
      14 |
      15 |   it('utils.promise timeout()', async () => {

      at Object.<anonymous> (packages/node-core/src/utils/promise.spec.ts:12:25)

Report generated by 🧪jest coverage report action from 1c4a497

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
packages/node-core/src/indexer/unfinalizedBlocks.service.ts (1)

337-340: ⚠️ Potential issue | 🔴 Critical

Height-equality assert can still crash fork recovery.

At Line 337, bestHeader.blockHeight === checkingHeader.blockHeight is not guaranteed: checkingHeader may come from registerUnfinalizedBlock()/backfill at a higher height than any bestVerifiableBlocks entry. This assert can terminate the process on the exact non-sequential/fork paths this PR targets.

Proposed fix
-    for (const bestHeader of bestVerifiableBlocks.reverse()) {
-      assert(
-        bestHeader.blockHeight === checkingHeader.blockHeight,
-        'Expect best header and checking header to be at the same height'
-      );
+    for (const bestHeader of bestVerifiableBlocks.reverse()) {
+      while (checkingHeader.blockHeight > bestHeader.blockHeight) {
+        assert(checkingHeader.parentHash, 'Expect checking header parentHash to be exist');
+        checkingHeader = await this.blockchainService.getHeaderForHash(checkingHeader.parentHash);
+      }
+
+      if (checkingHeader.blockHeight < bestHeader.blockHeight) {
+        continue;
+      }
+
       if (bestHeader.blockHash === checkingHeader.blockHash || bestHeader.blockHash === checkingHeader.parentHash) {
         return bestHeader;
       }

       // Get the new parent
       assert(checkingHeader.parentHash, 'Expect checking header parentHash to be exist');
       checkingHeader = await this.blockchainService.getHeaderForHash(checkingHeader.parentHash);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/node-core/src/indexer/unfinalizedBlocks.service.ts` around lines 337
- 340, The assert comparing bestHeader.blockHeight and
checkingHeader.blockHeight can crash on fork/non-sequential paths
(checkingHeader may be from registerUnfinalizedBlock/backfill); replace the
assert with a non-fatal conditional: if heights differ, do not throw—handle
gracefully by logging a warning and returning early (or otherwise following the
fork-recovery branch) instead of asserting, so the code can continue processing
bestVerifiableBlocks and unfinalized block backfill; update the block of logic
that currently uses bestHeader and checkingHeader to guard on equality and
choose the appropriate recovery path when they differ.
🧹 Nitpick comments (1)
packages/node-core/src/indexer/unfinalizedBlocks.service.spec.ts (1)

18-49: Best-height pruning test is not exercising the new event-driven path.

Line 279 updates mockBestHeight, but the service’s new logic reads best height from updateBestHeight() (BlockBest event), not blockchainService.getBestHeight(). This test can pass without validating the intended behavior.

Suggested test adjustment
-    mockBestHeight = 800;
+    unfinalizedBlocksService.updateBestHeight({height: 800});

Also applies to: 271-286

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/node-core/src/indexer/unfinalizedBlocks.service.spec.ts` around
lines 18 - 49, The test currently mutates mockBestHeight (used by
BlockchainService.getBestHeight) which bypasses the service’s new event-driven
path; instead, modify the test to emit the BlockBest event or call the service
method updateBestHeight() with the new height so the UnfinalizedBlocks service
reacts via its event handler; locate uses of mockBestHeight and replace the
direct mutation (around the existing assertions) with an explicit BlockBest
emission or invocation of updateBestHeight() and then await the service/state
update to assert pruning behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/node-core/src/indexer/unfinalizedBlocks.service.ts`:
- Around line 337-340: The assert comparing bestHeader.blockHeight and
checkingHeader.blockHeight can crash on fork/non-sequential paths
(checkingHeader may be from registerUnfinalizedBlock/backfill); replace the
assert with a non-fatal conditional: if heights differ, do not throw—handle
gracefully by logging a warning and returning early (or otherwise following the
fork-recovery branch) instead of asserting, so the code can continue processing
bestVerifiableBlocks and unfinalized block backfill; update the block of logic
that currently uses bestHeader and checkingHeader to guard on equality and
choose the appropriate recovery path when they differ.

---

Nitpick comments:
In `@packages/node-core/src/indexer/unfinalizedBlocks.service.spec.ts`:
- Around line 18-49: The test currently mutates mockBestHeight (used by
BlockchainService.getBestHeight) which bypasses the service’s new event-driven
path; instead, modify the test to emit the BlockBest event or call the service
method updateBestHeight() with the new height so the UnfinalizedBlocks service
reacts via its event handler; locate uses of mockBestHeight and replace the
direct mutation (around the existing assertions) with an explicit BlockBest
emission or invocation of updateBestHeight() and then await the service/state
update to assert pruning behavior.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 41c60dd and 88da492.

📒 Files selected for processing (2)
  • packages/node-core/src/indexer/unfinalizedBlocks.service.spec.ts
  • packages/node-core/src/indexer/unfinalizedBlocks.service.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
packages/node-core/src/indexer/unfinalizedBlocks.service.ts (1)

336-339: ⚠️ Potential issue | 🔴 Critical

Remove the same-height assert; it can crash valid fork handling paths.

Line 336 assumes checkingHeader.blockHeight === bestHeader.blockHeight, but checkingHeader may start above finalizedBlockNumber (e.g., from registerUnfinalizedBlock), so this assert can fail before reconciliation.

🔧 Suggested fix
-    for (const bestHeader of bestVerifiableBlocks.reverse()) {
-      assert(
-        bestHeader.blockHeight === checkingHeader.blockHeight,
-        'Expect best header and checking header to be at the same height'
-      );
+    for (const bestHeader of bestVerifiableBlocks.reverse()) {
+      while (checkingHeader.blockHeight > bestHeader.blockHeight) {
+        assert(checkingHeader.parentHash, 'Expect checking header parentHash to be exist');
+        checkingHeader = await this.blockchainService.getHeaderForHash(checkingHeader.parentHash);
+      }
+
+      if (checkingHeader.blockHeight < bestHeader.blockHeight) {
+        continue;
+      }
+
       if (bestHeader.blockHash === checkingHeader.blockHash || bestHeader.blockHash === checkingHeader.parentHash) {
         return bestHeader;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/node-core/src/indexer/unfinalizedBlocks.service.ts` around lines 336
- 339, Remove the hard assert that compares bestHeader.blockHeight and
checkingHeader.blockHeight and instead handle height mismatches gracefully: in
the reconciliation logic that currently uses assert(bestHeader.blockHeight ===
checkingHeader.blockHeight) (references: bestHeader, checkingHeader,
finalizedBlockNumber, registerUnfinalizedBlock), replace the assert with
conditional logic that allows checkingHeader to be above the
finalizedBlockNumber (e.g., if checkingHeader.blockHeight !==
bestHeader.blockHeight then either advance/choose the higher header or
skip/continue reconciliation for that branch and optionally log a debug message)
so valid fork handling paths do not crash.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/node-core/src/indexer/unfinalizedBlocks.service.ts`:
- Around line 336-339: Remove the hard assert that compares
bestHeader.blockHeight and checkingHeader.blockHeight and instead handle height
mismatches gracefully: in the reconciliation logic that currently uses
assert(bestHeader.blockHeight === checkingHeader.blockHeight) (references:
bestHeader, checkingHeader, finalizedBlockNumber, registerUnfinalizedBlock),
replace the assert with conditional logic that allows checkingHeader to be above
the finalizedBlockNumber (e.g., if checkingHeader.blockHeight !==
bestHeader.blockHeight then either advance/choose the higher header or
skip/continue reconciliation for that branch and optionally log a debug message)
so valid fork handling paths do not crash.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 88da492 and 1c4a497.

📒 Files selected for processing (1)
  • packages/node-core/src/indexer/unfinalizedBlocks.service.ts

@ianhe8x ianhe8x merged commit d5336c5 into main Feb 26, 2026
3 of 4 checks passed
@ianhe8x ianhe8x deleted the unfinalized-non-sequentential branch February 26, 2026 07:49
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