Skip to content

fix: 🐛 post-resync defects found testing redesign/07 against real chain data - #355

Open
prashantasdeveloper wants to merge 4 commits into
redesign/07-schema-invariantsfrom
fix/redesign-post-resync-defects
Open

prashantasdeveloper wants to merge 4 commits into
redesign/07-schema-invariantsfrom
fix/redesign-post-resync-defects

Conversation

@prashantasdeveloper

Copy link
Copy Markdown
Contributor

What

Three fixes found by resync-testing redesign/07-schema-invariants from genesis against
testnet — the first full resync of anything past Phase 4, so the first time several code paths
ever ran against real chain data. Branched off redesign/07-schema-invariants; base this PR on
it, not master.

1. SetAssetMetadataValue couldn't resolve its key through a batch or multisig proposal

resolveMetadataKey only inspected the outer, signed extrinsic. A setAssetMetadata(Details)
call reached via utility.batch* (the common "create an asset and set its metadata inline"
shape) or executed by multiSig.approve of a previously-created proposal never resolved — the
row was silently dropped with a MissingReferencedEntity anomaly instead of being written.

Root-caused against 12 real occurrences on testnet (specs 7002000/7004000/7004001/
8000000). Both new paths pick their call by ordinal position among same-extrinsic
SetAssetMetadataValue(Details) siblings (not by asset_id alone — a batch/proposal can set more
than one key, including twice for the same asset) and verify against the already-decoded
assetId as a consistency guard. parseMetadataKey also strips the thousands separators
toHuman() puts in a u64 ("1,234"), which would otherwise split a key's rows depending on
which path resolved it. Both of those were caught in an Opus review of the first version of this
fix, along with several test-coverage gaps — see the commit body for the full review trail.

Pre-existing since Phase 5 (redesign/05-holdings-nfts, 6149410); not introduced by phase 7.

2. A later v8 runtime names an event's fields in snake_case, crashing the indexer

Found live, mid-resync, as a genuine crash loop (not a candidate item — this blocked the resync
from ever completing). asset.Approval's metadata names its fields in Substrate's idiomatic Rust
snake_case (asset_id) on a later v8 testnet spec, not the camelCase (assetId) every shape
table and handler in this codebase reads. The decode layer's own doc comment asserted Polymesh
pallets are tuple-style (no field names) at every spec version — true until this runtime — so
there was no shape-table fallback once metadata named a field at all, and nothing caught the
resulting FieldNotFound at the handler call site.

Fixed at the root in metadataFieldNames, not as a one-event patch: normalise a metadata field
name to camelCase only when it contains an underscore, a no-op for the (previously universal)
already-camelCase case. Deployed directly to the resync server mid-run (code-only change, no
schema drop) to unblock it; confirmed it sailed through the stuck block cleanly.

3. A17: lazily-discovered accounts got approximate provenance

getOrCreateAccount defaulted createdEventId to the block's first event
(${blockId}/0000000000) for every account discovered while resolving an asset transfer's
from/to holder — accepted as a bounded phase-7 follow-up rather than an in-phase cascade (see
docs/implementation/13-entity-provenance.md).

Threaded the real blockEventId through the whole asset-holder resolution chain
(rawAssetHolderToAssetHolderextractAssetHoldermeshAssetHolderToAssetHolder
getOrCreateAccount) from every real call site: asset transfers/allowances, NFT transfers,
settlement legs and funds transfers. The portfolio-only paths never call getOrCreateAccount and
needed no change. A caller with no event to give still gets the old default.

Testing

  • yarn codegen && yarn typecheck && yarn lint && yarn test:unit && yarn check-handlers && yarn build
    — all green throughout (final: 0 type errors, 0 lint warnings, 512/512 unit tests).
  • Verified against a full genesis-to-tip testnet resync (redesign/07-schema-invariants +
    these fixes): reached chain head cleanly. Final indexer_anomalies table has exactly the 12
    rows item 1 explains — nothing else — and zero BalanceReconciliationDrift rows (better
    than expected; the phase-7 reconcile-hygiene fix left no drift at all across the whole run).

@prashantasdeveloper
prashantasdeveloper requested a review from a team as a code owner September 11, 2026 18:23
…g proposal

`resolveMetadataKey` only inspected the outer, signed extrinsic's method, so a
`setAssetMetadata(Details)` call reached via a `utility.batch*` extrinsic (the common shape for
"create an asset and set its metadata inline") or executed by `multiSig.approve` of a previously
created proposal never resolved — the row was silently dropped with a `MissingReferencedEntity`
anomaly instead of being written.

Root-caused against 12 real occurrences (testnet genesis-to-tip resync, specs 7002000/7004000/
7004001/8000000): most were `utility.batch*` wrapping a direct `setAssetMetadata` call alongside
`createAsset`/`registerUniqueTicker`/etc; the rest were `multiSig.approve` executing an
`asset.set_asset_metadata` proposal, whose call the indexer already captured in
`MultiSigProposal.params.proposals` at `ProposalAdded` time (`multiSig.proposals` on-chain storage
is cleared once a proposal executes, so that's the only place left to read it from).

Both paths pick their call by ordinal position among same-extrinsic `SetAssetMetadataValue(Details)`
siblings, then check it against the already-decoded `assetId` as a consistency guard — matching by
`assetId` alone (an earlier version of this fix, caught in review) picks the wrong call whenever a
batch or proposal sets more than one key, including twice for the same asset. `parseMetadataKey`
also strips the thousands separators `toHuman()` puts in a `u64` (`"1,234"`), another review catch
that would otherwise have split a key's rows depending on which path resolved it.
Crashed the redesign/07 resync in a genuine, non-transient loop: block 25,557,789's
`asset.Approval` — "has no field \"assetId\"; it carries [owner, spender, asset_id, amount]" —
repeating on every retry, `rc` climbing (11 restarts and counting) with no way to get past it.

The decode layer's own doc comment says Polymesh pallets emit tuple-style events (no field names
at any spec version) and only upstream Substrate pallets emit named ones — the registered shape
table and every handler's destructuring are camelCase-only on that assumption. A later v8 testnet
runtime breaks it: `asset.Approval`'s metadata now names its fields, in Substrate's own idiomatic
Rust snake_case (`asset_id`), not the camelCase every handler reads. Once metadata names a field,
`decodeEvent` uses those names as-is and never falls back to the (correctly camelCase) shape
table, so the mismatch is a guaranteed `FieldNotFound` — thrown by design, but nothing catches it
at the handler call site, so it takes the worker down instead of just recording the anomaly it
already writes on the way out.

Fixed at the root, in `metadataFieldNames`, rather than in the one handler that happened to hit
it: normalise a field name to camelCase only when it contains an underscore, so a name that is
already camelCase (everything decoded successfully so far) passes through unchanged.
@prashantasdeveloper
prashantasdeveloper force-pushed the fix/redesign-post-resync-defects branch from 40de904 to f0c7a3b Compare September 11, 2026 18:25
`getOrCreateAccount` defaulted `createdEventId` to the block's first event (`${blockId}/0000000000`)
whenever a caller had no real one to give — every call reached through the asset-holder resolution
chain (`rawAssetHolderToAssetHolder` -> `extractAssetHolder` -> `meshAssetHolderToAssetHolder`), the
path that lazily discovers an Account/IdentityKey while resolving a transfer's from/to holder.
Accepted as a bounded follow-up during phase 7 rather than an in-phase cascade (see
docs/implementation/13-entity-provenance.md and the phase-7 decisions log).

`meshAssetHolderToAssetHolder` / `extractAssetHolder` / `rawAssetHolderToAssetHolder` now take an
optional `blockEventId`, threaded from every real call site that already extracts one — asset
transfers and allowances (mapAsset.ts), NFT transfers (mapNfts.ts), settlement legs and funds
transfers (mapSettlement.ts, settlements.ts) — so a lazily-created Account's `createdEventId`
records the event that actually caused the discovery. A caller with no event to give still gets
the old block-event-0 default; `getOrCreateAccount`'s signature is unchanged.

The portfolio-only paths (`meshPortfolioToAssetHolder` / `rawPortfolioToAssetHolder`, used across
mapPortfolio.ts, mapIdentities.ts, stos.ts, distributions.ts, the seed) never call
`getOrCreateAccount` and are untouched — they build a `PortfolioDetails` from already-decoded
fields, no account resolution involved.
…t:S7776)

`BATCH_METHODS.includes(...)` is an O(n) scan of a 6-element array on every batched
SetAssetMetadataValue(Details); a Set's `.has()` is O(1) and is what Sonar's own rule asks for.
@prashantasdeveloper
prashantasdeveloper force-pushed the fix/redesign-post-resync-defects branch from f0c7a3b to fa27b92 Compare September 11, 2026 18:36
@sonarqubecloud

Copy link
Copy Markdown

@prashantasdeveloper
prashantasdeveloper added this pull request to stack #358 September 15, 2026 08:23
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