Skip to content

fix: 🐛 fixes from the resync of #383 and #382: bare accounts, error names, fork blocks - #385

Merged
polymesh-bot merged 4 commits into
docs/architecturefrom
redesign/25-sync-fixes
Oct 28, 2026
Merged

polymesh-bot merged 4 commits into
docs/architecturefrom
redesign/25-sync-fixes

Conversation

@F-OBrien

Copy link
Copy Markdown
Contributor

Why

The resync of #383 and #382 from genesis on testnet and mainnet turned up three problems that aren't in the expected differences:

  1. Testnet: 19 onboarded accounts lost their identity on their first fee. An authorization creates its target's Account as a bare Unlinked row (fix!: 🐛 match the chain's state where upgrades, silent calls and decodings left the index wrong #383). When the target was later onboarded, nothing linked that row until DidCreated. So the Endowed before it, in the same block, was recorded with no identity, and those identities and portfolios started at DidCreated rather than at the block's first event.
  2. Testnet: 23 instructions' failure reason changed from run to run. These are the 23 that failed at 10,036,148, the first block on 6.0. Some runs read DeprecatedCallOnV2Instruction, which is 5.4's name for error 37/0x22; others read 6.0's FailedToReleaseLockOrTransferAssets.
  3. Both chains kept 2 fork blocks each after reaching head. On mainnet, each one's fee went to the fork's block author, which is the 4 head check-state differences.

What

Link a bare account when its key record names it

getOrCreateAccount returned any existing row as it was. Now an existing row that is Unlinked with no identity reads the key record, as a missing row does, and is linked through upsertAccount when the chain names the address as a key. The row keeps its createdEventId.

The key record is still read at most once per address per block, and testnet has only 711 such rows. This also covers an address that held POLYX before it was onboarded.

Name a dispatch error from the runtime that decoded it

The dictionary starts most specs up to 300 blocks late. On those blocks the node builds the mappings' api with the previous runtime's metadata, so api.registry.findMetaError can name another runtime's error.

The event itself is decoded at its own block's runtime: the node fetches events with system.events.at(hash). So getErrorDetails now uses the codec's own registry, and the two pre-6.0 silent-failure paths pass theirs along.

The dictionary's late starts are fixed in PolymeshAssociation/polymesh-subquery-dictionary#8, together with a one-off correction of its stored rows. This change makes error names right whatever the dictionary says.

Rewind fork blocks

The fork blocks were kept by @subql/node-core 19.0.0, which @subql/node 6.4.6 pins:

  • it never checks an unfinalized block's parent hash;
  • it compares only the highest stored block at or below finality with the chain;
  • so a fork block followed by a canonical one is never rewound.

19.2.0 added the parent-hash check (subquery/subql#3016), but it exits on most of those forks instead of rewinding them. On testnet that happened minutes after reaching head. The fix is upstream in subquery/subql#3052. Until a @subql/node release includes it, the image:

Verification

  • Unit tests: 3,798 pass, with new tests for the first two fixes. Lint, check-strict-null and yarn build pass.
  • The failure reason: at 10,036,148, the event's own registry is 6.0's and names the error FailedToReleaseLockOrTransferAssets; the registry of the block before names it DeprecatedCallOnV2Instruction.
  • The image, against testnet head:
    • 19.3.1 unpatched: the node exited at 26,457,518 on Expect best header and checking header to be at the same height. On restart it rewound the fork block 26,457,517.
    • 19.3.1 patched: the node rewound a fork at 26,458,389 to 26,458,391 without exiting. Afterwards its unfinalized blocks all matched the canonical chain.
  • Full resync: staging, testnet and mainnet are next.

An authorization creates its target's Account as a bare Unlinked row, and
getOrCreateAccount returned an existing row as it was, so a target onboarded
later stayed unlinked until DidCreated. The Endowed before DidCreated then had
no identity: on testnet 19 fee entries, and their identities and portfolios
started at DidCreated rather than at the block's first event.

A row that is Unlinked with no identity now reads the key record, once per
block, and is linked when the chain names the address as a key. An address
that held POLYX before it was onboarded is covered by the same path.
On a block the node labels with the neighbouring runtime's spec, api.registry
holds that runtime's metadata, so a module error read through it can take
another runtime's name. Testnet's 23 instructions that failed at 10,036,148,
the first block on 6.0, read DeprecatedCallOnV2Instruction (5.4's 37/0x22)
in runs where the block was labelled 5.4, and FailedToReleaseLockOrTransferAssets
(6.0's) otherwise. The event is decoded at its own block's runtime, so its
codec's registry names the error that runtime raised.
…ld keep

node-core 19.0.0, which @subql/node 6.4.6 pins, never checked an unfinalized
block's parent hash, and compared only the highest stored block at or below
finality with the chain. A fork block followed by a canonical one was kept: 2
on each chain after the last resync, and on mainnet their fees went to the
fork's authors. 19.2.0 checks the parent hash (#3016); 19.3.1 also fixes a SQL
injection in dynamic datasources (#3031).

The node runs from the upstream image's node_modules, so the image replaces
node-core there; the resolution keeps local runs on the same version.
@F-OBrien
F-OBrien requested a review from a team as a code owner October 27, 2026 17:12
@F-OBrien
F-OBrien force-pushed the redesign/25-sync-fixes branch from 383d5b3 to 0057877 Compare October 27, 2026 17:18
From 19.2.0, a block whose parent hash doesn't match the last stored block
reports a fork from above the finalized height, and the search for the last
valid block asserts that it is at the height of a stored block at or below
finality. On most forks it isn't, so the node exits instead of rewinding:
testnet 26,457,517, minutes after reaching head. A restart rewinds it, so the
data stays right, but the node would restart several times an hour.

patch-node-core.js makes that fork only a signal to rewind, as the upstream
fix now does: hasForked checks the stored blocks at or below finality against
the finalized chain and picks the target, or the finalized block when none is
stored. Walking back from the reported header instead costs a request per
block, and can land on a stored fork block when the backfill reports the
fork. The build fails if 19.3.1's compiled code isn't what the patch replaces.
@F-OBrien
F-OBrien force-pushed the redesign/25-sync-fixes branch from 0057877 to 966b4a8 Compare October 28, 2026 10:57
@sonarqubecloud

Copy link
Copy Markdown

@prashantasdeveloper

Copy link
Copy Markdown
Contributor

/fast-forward

@polymesh-bot
polymesh-bot merged commit 966b4a8 into docs/architecture Oct 28, 2026
8 checks passed
@polymesh-bot
polymesh-bot deleted the redesign/25-sync-fixes branch October 28, 2026 12: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.

3 participants