Skip to content

fix(js): validate route_idx in v2 header check; uniform events[x].type#1340

Merged
vobradovich merged 5 commits into
masterfrom
fix/js-route-idx-and-event-type
May 7, 2026
Merged

fix(js): validate route_idx in v2 header check; uniform events[x].type#1340
vobradovich merged 5 commits into
masterfrom
fix/js-route-idx-and-event-type

Conversation

@ukint-vs
Copy link
Copy Markdown
Member

@ukint-vs ukint-vs commented May 5, 2026

Summary

Two related sails-js bugfixes from open issues without linked PRs.

Changes

Commit What
ef074b87 _assertMatchingHeader (v2) extended with route_idx validation.
3b02d760 events[x].type (v2) routes the already-computed typeStr to the public field; drops the unused intermediate t.
608ae352 Same one-line fix applied to v1 events at js/src/sails.ts:264. Existing service.test.ts events test gains a named-field event variant.
77549ada Cleanup pass: replace local byte-concat with u8aConcat from @polkadot/util, hoist parser.parse(idl) into beforeAll, drop one as any cast, trim two over-explanatory comments.
5383b328 Codex review follow-up: honor received.routeIdx === 0 as the spec's inference sentinel (was previously rejected when expected was concrete); apply the same route check to events[x].is() and the subscribe predicate so they no longer disagree with decode() in multi-route programs.

Decisions worth noting

  • Symmetric route_idx rule (after Codex review): throw only when both sides assert concrete (non-zero) routes that disagree. Either side being 0 is the inference sentinel per docs/sails-header-v1-spec.md §6.2 line 78 and §13.6 line 128 — accepted in both directions. This still catches the original sails-js: _assertMatchingHeader should validate route_idx for multi-mounted services #1316 bug (RMRK-style off-route delivery) while preserving spec-compliant inference for legacy/single-instance receivers.
  • is() and subscribe parity: both predicates now share matchesRoute() with decode(), so the common if (event.is(e)) event.decode(e) pattern is consistent. Without this, is() returned true for the wrong service while decode() threw — a confusing surface.
  • decodeResult not covered here: it doesn't call _assertMatchingHeader on master. PR fix(js): validate reply prefix in decodeResult #1315 (fix/js-decoderesult-header) adds that call site; once it lands, decodeResult automatically inherits this PR's route_idx validation.
  • sails-js v2: named-field event .type is an object while unit/unnamed variants return strings #1318 is a runtime breaking change for consumers reading .type.fields on named-field events. The documented TS type is any, so it's not a TypeScript-level break. Migration: read event.typeDef.fields (already public, unchanged).

Tests

15 new test cases + 1 extended test case across 3 files. All pass:

$ pnpm jest --runInBand --testPathPatterns "route-idx-header-validation |event-type-shape|service.test"
Test Suites: 3 passed, 3 total
Tests:       17 passed, 17 total

Test plan:

  • Function decodePayload rejects mismatched concrete route_idx with "Header mismatch" referencing both expected and got values
  • Event decode rejects mismatched concrete route_idx
  • Ctors accept any received route_idx (asymmetric expected-only rule)
  • Received route_idx === 0 is accepted as the inference sentinel (function + event)
  • event.is() returns false for wrong concrete route_idx (consistent with decode())
  • event.is() returns true for inference sentinel route_idx === 0
  • Matching route_idx decodes successfully (positive case)
  • events[x].type is a string for unit, tuple, and named-field variants (v2)
  • Named-field event round-trips through event.decode(...) after the fix
  • event.typeDef.fields escape hatch still works
  • v1 service.test.ts extended with named-field event — pre-fix returned object, post-fix returns string
  • Existing offline test suites (partial-idl, idl-v2-type-resolver, encode-decode, enum, struct) still pass

Review

  • Local /review pass: clean
  • Codex review (round 1): 2 [P2] findings, both addressed in 5383b328
  • Lint: ESLint clean

Closes #1316
Closes #1318

🤖 Generated with Claude Code

ukint-vs and others added 4 commits May 5, 2026 15:42
The v2 header-validation helper compared interface_id and entry_id but
silently ignored route_idx. For programs that mount the same interface at
two different routes (RMRK-style routing, multi-tenant services,
upgrade-in-place during cutover), an off-route reply or event passed
validation when decoded by the wrong service instance.

Per docs/sails-header-v1-spec.md, route_idx is a first-class routing
identifier and 0x00 is the inference sentinel. The Rust client at
rs/src/client/mod.rs:418-419,:872-873 already validates strictly; this
brings JS in line.

Asymmetric expected-only rule: throw when received != expected unless
expected is 0 (the ctor decode path, which has no service route). This
matches Rust's behavior for service paths and exempts ctors cleanly.

decodeResult will inherit the same validation automatically once #1315
(fix/js-decoderesult-header) lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The v2 events map exposed `events[x].type` as a getStructDef *object* for
named-field event variants but as a string for unit and tuple variants.
Consumers that wanted a single string representation either had to
special-case the named-field case or got `[object Object]`.

The string form already existed in the same code path — `typeStr` at
sails-idl-v2.ts:507 was computed and immediately discarded. This change
just routes it to the public `type` field instead of the unused
intermediate variable.

BREAKING (runtime): consumers that did `events[x].type.fields` on
named-field events will now get undefined. Migration: read
`events[x].typeDef.fields` (already public, unchanged). The documented
TypeScript type of `.type` is `any`, so this is not a compile-time break.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The same `events[x].type` object/string inconsistency exists at
js/src/sails.ts:264 in the v1 Sails class — `getScaleCodecDef(type)`
returns an object for non-tuple structs (js/util/src/types.ts:130-134),
while `getScaleCodecDef(type, true)` stringifies. The intermediate `t`
was never read elsewhere; removing it and routing `typeStr` to
`events[x].type` matches the v2 fix one-for-one.

Issue #1318 is titled "v2", but the v1 path has the identical bug. Same
breaking note applies: consumers reading `.type.fields` on named-field
events need to switch to `.typeDef`.

Existing service.test.ts events test gains a named-field event variant
(`Walked: struct { from: u32, to: u32 }`) — pre-fix it returned an
object, post-fix it's a string.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cleanups surfaced by /simplify pass over the #1316/#1318 fixes:

- Replace local `concatBytes` helper and inline 3-line byte concatenation
  in both new test files with `u8aConcat` from `@polkadot/util`.
- Hoist `new SailsProgram(parser.parse(idl))` from each test into
  `beforeAll`. The IDL is module-level and parses identically per call;
  re-parsing 5x per file was unnecessary work and kept the WASM linear
  memory clear loop hot.
- Drop `(event.typeDef as any).entry_id` cast — `IServiceEvent` /
  `IEnumVariant` already declares `entry_id?: number`.
- Trim the route_idx comment to just the spec citation; drop the
  redundant "skip equality for ctors" half (the code already says that).
- Drop the regression-narrative comment on `service.test.ts:163` —
  the `expect(typeof ...).toBe('string')` line is self-documenting.

No behavior change. 14 tests still pass across the three affected suites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements route index validation within the header matching logic, specifically allowing a sentinel value of zero for constructors to support inference. Additionally, it standardizes the representation of event types to ensure they are consistently returned as strings across both v1 and v2 IDL paths, resolving previous inconsistencies with named-field variants. Comprehensive test suites have been added to verify the new validation logic and the uniform shape of event types. I have no feedback to provide as the changes correctly address the requirements and include appropriate test coverage.

@ukint-vs ukint-vs self-assigned this May 5, 2026
@ukint-vs ukint-vs requested review from osipov-mit and vobradovich May 5, 2026 13:18
…o event predicates

Codex review surfaced two follow-ups to the prior route_idx fix:

1. _assertMatchingHeader rejected received route_idx=0 when expected was
   non-zero. Per docs/sails-header-v1-spec.md §13.6, received route_idx=0 is the
   inference sentinel and must be accepted. Now both sides are symmetric: throw
   only when both are non-zero and disagree.

2. events[x].is() and the subscribe predicate matched on interface_id+entry_id
   only, so they returned true for the wrong service in multi-route programs
   while decode() then threw — an inconsistent surface. Both predicates now
   share the same matchesRoute() check used by the decode path.

Tests added:
- received route_idx=0 decodes successfully (function + event)
- event.is() returns false for wrong concrete route_idx
- event.is() returns true for inference sentinel route_idx=0
@vobradovich vobradovich merged commit 9a79c95 into master May 7, 2026
2 checks passed
@vobradovich vobradovich deleted the fix/js-route-idx-and-event-type branch May 7, 2026 09:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants