Skip to content

fix: 🐛 fix what chain resyncs of the redesign found outside the POLYX ledger - #367

Open
F-OBrien wants to merge 11 commits into
redesign/12-review-fixesfrom
redesign/12a-chain-fixes
Open

F-OBrien wants to merge 11 commits into
redesign/12-review-fixesfrom
redesign/12a-chain-fixes

Conversation

@F-OBrien

@F-OBrien F-OBrien commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Stack: #361 (review fixes) → #367 (resync fixes outside the ledger) → #368 (POLYX ledger resync fixes) → #369 (ledger split by era) → #365 (consumer requests)

Description

Fixes found by running PR 361 against testnet and mainnet, outside the POLYX ledger. It sits on #361. The ledger fixes are in #368.

Each commit is self-contained and its message records the chain evidence (blocks, specs, counts), so it reads best commit by commit.

Settlement and assets

  • 6bc6e52 Before v6, asset.Transfer names no instruction, so every transfer in a block was filed under its first InstructionExecuted (testnet block 5,091,763: 27 instructions, 270 transfers, all filed under one). Each transfer now takes the next InstructionExecuted in its phase. Adding an affirmation's signer to the legs was quadratic in a block of affirmations; Instruction.legCount lets the legs be read by id.
  • 52cf2b4 An unrecognised event name following a transfer reached the event_id enum column and halted indexing. It now maps through toEnum to Unknown.
  • 63469dd CorporateBallot.corporateAction becomes nullable and is set only when the action is indexed, matching Distribution.corporateAction.

Identities and governance

  • 5912165 The genesis config grants ten CDD claims that no ClaimAdded announces, so the index lacked all ten on mainnet. They are now seeded from identity.claims at genesis.
  • e3fe724 propose schedules a PIP's expiry before ProposalCreated, so the expiry was always dropped (every mainnet PIP with an expiry). ProposalCreated now takes it from the earlier ExpiryScheduled.
  • 5121075 AssetAgentAction records SetAccountFreeze, FrozenBalanceSet, ControllerTransferTo, TickerLinkedToAsset and TickerUnlinkedFromAsset. These were missing from the agent-permissioned calls. Moved here from PR 365.

Schema clean-up

  • 8118c72 Removes FoundType and Debug (plan 09 §9.7). FoundType was rewritten once per event argument: 1.4M row versions for 403 names on a testnet resync.
  • 9039c64 Drops updatedEvent from AssetAgentHistory, which is append-only and was missed when the other append-only entities lost it.
  • fcd7c4b Drops Holding's (portfolio, asset) composite index, which duplicates the row's id.
  • 67c62e5 Renames utils/transferManagers.ts to compliance.ts.

Docs

  • 3c77a95 Plan 09 §9.10 proposes one canonical encoding for event and call arguments. It is breaking for the SDK and the portal, so it is recorded as pending a decision.

Breaking Changes

  • FoundType and Debug entities removed. Neither the SDK nor the portal reads them.
  • AssetAgentHistory.updatedEvent removed. Use createdEvent.
  • CorporateBallot.corporateAction is nullable.

The redesign already needs a fresh reindex; these come with it.

Verification

typecheck, test:unit, lint, check-handlers, check-strict-null and build pass on this tip.

JIRA Link

None.

Checklist

  • Updated the Readme.md (if required)? Not required.

Both are development instrumentation in the production schema, marked
for removal in the redesign (docs/implementation/09-infrastructure.md
§9.7).

FoundType cost a write per argument of every event: the serializer
saved the same id again for each one. A testnet genesis resync left
1.4M row versions for 403 type names, and closing each version scanned
the whole table, 166 ms a time. Nothing has written Debug for some time.

BREAKING CHANGE: the FoundType and Debug entities and their queries are
removed. Neither the SDK nor the portal reads them.
Before v6, asset.Transfer names no instruction, and handleAssetTransfer
took the block's first InstructionExecuted. In a block executing several
instructions every transfer was filed under the first: testnet block
5,091,763 executed 27 and all 270 transfers went to one.

asset.Transfer is emitted by unsafe_transfer, which only three paths
reach (checked at v3.0.0, v4.1.0 and v5.4.0): settlement, which
transfers an instruction's legs and then emits InstructionExecuted in
the same dispatch, with only further transfers and CheckpointCreated
between; asset.controller_transfer, followed by ControllerTransfer; and a
capital distribution claim, followed by BenefitClaimed. A transfer's
instruction is now the next InstructionExecuted in its phase, a
controller transfer or claim has none, and anything else is recorded as
an UnreadableValue anomaly. All 16,488 pre-v6 testnet transfers
classify, none unexplained, and every settlement transfer is filed
under its own instruction.

Adding an affirmation's signer to an instruction's legs searched for the
legs with getByFields, quadratic in a block of affirmations (testnet
block 5,091,762 has 498). Instruction now records legCount, and the legs
are read by id, instructionId/0 to legCount - 1, all at once. Fewer legs
than that, or an affirmed instruction the index does not hold, is
recorded as a MissingReferencedEntity anomaly.
AssetAgentHistory is append-only: every handler creates a row and none
reads one back to change it, so updatedEvent always equalled
createdEvent. It was missed when updatedEvent was dropped from the other
append-only entities, and it still cost a non-null foreign key and its
index on every row.

BREAKING CHANGE: AssetAgentHistory.updatedEvent is removed. Select
createdEvent instead.
For a portfolio holding the pair is the row's id, assetId/portfolioId, so
a filter on both is already a point lookup, and portfolio alone has the
index every relation gets. Under historical state each index is a GiST
index maintained on every insert, on a table written for every asset
movement. The same reasoning removed Nft's (asset, nftId) composite.

The (asset, identity) composite stays: it serves one identity's holdings
of one asset across its portfolios and accounts, the total the
AssetHolder rollup has to agree with. Its docstring now says so, and that
it has not been measured.
The transfer-manager helpers went when the pre-v5 transfer-manager path
was removed. What is left parses compliance requirements and transfer
compliance exemptions, so the file is named for that.
CorporateBallot.corporateAction was non-null and set from the ballot's
own id without checking the action exists. attach_ballot names a
corporate action created by an earlier extrinsic, so when that action
is outside the index (an index started after it, or a missed
CAInitiated) the ballot pointed at a row that is not there, which a
non-null relation only fails on at query time.

The relation is now nullable and set only when the action is found;
otherwise it is left null and a MissingReferencedEntity anomaly is
recorded, as Distribution.corporateAction already does.
propose schedules a PIP's expiry before it emits ProposalCreated, in
every runtime from v3.0.0 to v8.1.2, so ExpiryScheduled arrives first,
before the PIP exists. Its handler found no proposal, recorded a
MissingReferencedEntity anomaly and dropped the expiry: on mainnet,
every PIP proposed with an expiry, from PIP 0 at block 68,657.

ProposalCreated now takes the expiry from the ExpiryScheduled earlier
in its own extrinsic, and the separate handler is gone. A PIP whose
expiry the scheduler refused emits ExpirySchedulingFailed instead, and
still records none.
An instruction-less Transferred balance update is labelled with the name
of the event that follows it. The name was cast to EventIdEnum unchecked,
so an event the enum lacks reached the asset_transactions.event_id
insert, failed the enum check, and halted indexing on that block. Map it
through toEnum, as extractArgs does, so unknown names become Unknown.

The same fix as alpha's 5c36f8b, whose test here expects the redesign's
zero-padded instruction id.
The genesis config gives genesis and system identities CDD claims,
issued by the governance committee and the CDD system identity, and no
ClaimAdded announces them. The genesis seed created those identities
but no claims, so the index lacked all ten on mainnet. When PIP 149
revoked one at block 20,842,777, the revocation found no claim and
recorded an anomaly; the other nine are still on chain.

The genesis handler now reads identity.claims at the genesis block and
writes each claim through the same path as ClaimAdded, after the
identities they are about. The full double-map scan helper moves to
utils so the holdings seed and this one share it.
Plan 09 §9.10 proposes replacing the harvester-era event encoding, the
positional eventArg columns and Extrinsic.paramsTxt with arguments built
on toPrimitive() and an EventReference lookup. It is breaking for the SDK
and the portal, so it is recorded as pending a decision, with the
consumer changes listed in reference/consumer-queries.md §5.
…ent actions

AssetAgentAction is the per-asset record of what an agent did, but five
agent-permissioned asset calls were missing from it, so "who froze this
holder, and when" had no answer there:

- SetAccountFreeze and FrozenBalanceSet (8.1.1), which freeze a holder
  outright or partly;
- ControllerTransferTo (8.1.1), a forced transfer to a named holder;
- TickerLinkedToAsset, listed but commented out, and
  TickerUnlinkedFromAsset, never listed. Both require the agent's
  permission in every runtime from v7.0.

Each is now recorded with its caller and asset. The holder and amount
stay on the action's event.
@F-OBrien
F-OBrien requested a review from a team as a code owner October 7, 2026 17:13
@sonarqubecloud

sonarqubecloud Bot commented Oct 7, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
3 New Code Smells (required ≤ 0)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

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