Skip to content

[2.x] Reject duplicate set items in dynamic codecs - #1208

Draft
lorisleiva wants to merge 1 commit into
10-05-upgrade_v1_instructions_programs_and_roots_to_v2from
10-05-reject_duplicate_set_items_in_dynamic_codecs
Draft

lorisleiva wants to merge 1 commit into
10-05-upgrade_v1_instructions_programs_and_roots_to_v2from
10-05-reject_duplicate_set_items_in_dynamic_codecs

Conversation

@lorisleiva

@lorisleiva lorisleiva commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

This PR makes setTypeNode encoders reject items that encode to the same bytes, which restores the uniqueness check that v1's dynamic-client got from superstruct.

codec.encode([42, 99, 42n]);
// throws DYNAMIC_CLIENT__DUPLICATE_SET_ITEM with { firstIndex: 0, index: 2, nodePath }
  • Items are compared by their encoded bytes, so the check works for any item type and takes encoded default values into account.
  • Decoding keeps duplicates, so on-chain data is shown as it is.
  • New error CODAMA_ERROR__DYNAMIC_CLIENT__DUPLICATE_SET_ITEM (2500021).

@changeset-bot

changeset-bot Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 7c1d78c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@lorisleiva lorisleiva changed the title Reject duplicate set items in dynamic codecs [2.x] Reject duplicate set items in dynamic codecs Oct 5, 2026
@lorisleiva

Copy link
Copy Markdown
Member Author

@trevor-cortex

@trevor-cortex trevor-cortex left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Summary

Adds an encode-time uniqueness check to setTypeNode codecs in @codama/dynamic-codecs. visitSetType now wraps the array-like codec in a new assertUniqueItems pre-encode transform that encodes each item, keys it by its base16 string, and throws the new CODAMA_ERROR__DYNAMIC_CLIENT__DUPLICATE_SET_ITEM (2500021) with { firstIndex, index, nodePath } on the first collision. Decoding is untouched. The error code, context type and message are registered in @codama/errors, and the tests cover integer/bigint equivalence, tuples, structs with defaulted fields, nested paths and decode passthrough.

The implementation is sound. Comparing by encoded bytes is the right call here — it sidesteps structural equality on arbitrary JS values and naturally handles 42 vs 42n, omitted-default fields, and item-level transforms (since item is the already-transform-wrapped codec from visit(node.item, this)). The non-array passthrough keeps the existing UNEXPECTED_VALUE_TYPE error for bad inputs, and because the check is applied inside visitSetType, the set's own transforms still layer on top via the intercept visitor. Order of errors is also sensible: a wrong-typed item throws the item's own UNEXPECTED_VALUE_TYPE from item.encode before the uniqueness check can run, which is the same error the inner codec would have produced.

Things to address

  • README is now out of date. packages/dynamic-codecs/README.md describes SetTypeNode as "Same as arrays." in the value-format table, and the "Invalid values" section only documents UNEXPECTED_VALUE_TYPE. Both should mention that encoding rejects duplicate items with DUPLICATE_SET_ITEM, since that's exactly the kind of behavioural difference a consumer would look up there.
  • No changeset in the diff. Per CONTRIBUTING.md, user-facing changes need one, and this touches two publishable concerns: @codama/dynamic-codecs gains a new encode-time rejection, and @codama/errors (fixed group) gains a new code. If the seeded major changeset is intended to absorb this during the rc candidacy, ignore this — but a dedicated entry would make the behaviour change discoverable in the changelog.

Notes for other reviewers

  • See the inline note on assertUniqueItems: because transformCodec's pre-encode hook runs in both getSizeFromValue and write, variable-size sets end up encoding every item twice for the check on top of the real encode. Not a correctness issue and almost certainly fine for a dynamic codec, just worth knowing about.
  • Worth a quick sanity check that CodamaError stores __code on context the way the duplicateSetItemError test helper assumes (context: { __code, ...context } with exact matching). It mirrors Kit's SolanaError, so I expect it does, and the tests would fail loudly otherwise.

Comment thread packages/dynamic-codecs/src/validation.ts
Comment thread packages/dynamic-codecs/test/codecs/SetTypeNode.test.ts
@lorisleiva
lorisleiva force-pushed the 10-05-reject_duplicate_set_items_in_dynamic_codecs branch from a1a4ae3 to 7c1d78c Compare October 5, 2026 17:21

@trevor-cortex trevor-cortex left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Re-review

Everything from the first pass has been addressed:

  • README now flags the duplicate rejection in the SetTypeNode row and documents DUPLICATE_SET_ITEM (with its index / firstIndex / nodePath context and the decode-keeps-duplicates caveat) in the "Invalid values" section. Reads well and matches the implementation.
  • assertUniqueItems docblock now calls out the extra encode per item and the double run on variable-size codecs, so the cost is discoverable.
  • Tests cover the two gaps I raised: non-array input still surfaces UNEXPECTED_VALUE_TYPE with expectedType: 'array' and the set's own nodePath, and a wrong-typed item throws the item's error with nodePath: [set, item] before any duplicate check runs. The [1, 'x', 'x'] input is a nice choice since it would otherwise be a duplicate, which pins the precedence properly.

Code is unchanged in substance and still looks correct: base16 keying of item.encode(...) output, first-collision reporting, non-array passthrough, and stack.getPath() captured at visit time consistent with assertValueType.

The only open item is the changeset, which is still absent from the diff. As before, if the seeded major changeset for the rc pre-release is meant to absorb this, that's fine — otherwise @codama/dynamic-codecs and @codama/errors both have user-visible changes here that would warrant an entry. Not blocking on it since that's a release-workflow call for you to make.

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.

2 participants