Skip to content

Conversation

@mcintyre94
Copy link
Member

@mcintyre94 mcintyre94 commented Oct 6, 2025

Problem

When an app has access to many codecs, for example when using generated clients, it is easy to write code that accidentally uses the wrong type of Decoder for a given byte array. This is difficult to debug as you will often be able to use the wrong decoder without any errors, but will be getting invalid data.

Summary of Changes

This PR adds a new helper function createDecoderThatUsesExactByteArray that can wrap any decoder. The transformed decoder throws if the new offset after decoding a byte array, is not at the end of the input bytes.

This means that if you pass a byte array that is longer or shorter than the wrapped decoder expects, the decode will fail.

As noted in the issue this is not a strong guarantee that you're using the correct decoder, many decoders are the same size or variable size. But it should catch a lot of difficult to debug cases in practice, such as decoding account data using the wrong decoder.

Note that to enable this, the signature of transformDecoder is modified to additionally provide the newOffset to the callback. We use both the original offset and the new offset in the error context.

Fixes #755

@changeset-bot
Copy link

changeset-bot bot commented Oct 6, 2025

🦋 Changeset detected

Latest commit: 7635876

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 41 packages
Name Type
@solana/codecs-core Patch
@solana/errors Patch
@solana/accounts Patch
@solana/addresses Patch
@solana/codecs-data-structures Patch
@solana/codecs-numbers Patch
@solana/codecs-strings Patch
@solana/codecs Patch
@solana/compat Patch
@solana/instructions Patch
@solana/keys Patch
@solana/options Patch
@solana/react Patch
@solana/rpc-api Patch
@solana/rpc-types Patch
@solana/signers Patch
@solana/transaction-confirmation Patch
@solana/transaction-messages Patch
@solana/transactions Patch
@solana/assertions Patch
@solana/instruction-plans Patch
@solana/kit Patch
@solana/programs Patch
@solana/rpc-spec Patch
@solana/rpc-subscriptions-channel-websocket Patch
@solana/rpc-subscriptions-spec Patch
@solana/rpc-subscriptions Patch
@solana/rpc-transformers Patch
@solana/rpc-transport-http Patch
@solana/rpc Patch
@solana/subscribable Patch
@solana/sysvars Patch
@solana/rpc-graphql Patch
@solana/rpc-parsed-types Patch
@solana/rpc-subscriptions-api Patch
@solana/fast-stable-stringify Patch
@solana/functional Patch
@solana/nominal-types Patch
@solana/promises Patch
@solana/rpc-spec-types Patch
@solana/webcrypto-ed25519-polyfill Patch

Not sure what this means? Click here to learn what changesets are.

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

Copy link
Member Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@bundlemon
Copy link

bundlemon bot commented Oct 6, 2025

BundleMon

Files updated (7)
Status Path Size Limits
errors/dist/index.node.mjs
15.73KB (+219B +1.38%) -
errors/dist/index.browser.mjs
15.71KB (+218B +1.37%) -
errors/dist/index.native.mjs
15.71KB (+218B +1.37%) -
codecs-core/dist/index.browser.mjs
3.41KB (+105B +3.1%) -
codecs-core/dist/index.native.mjs
3.41KB (+105B +3.1%) -
codecs-core/dist/index.node.mjs
3.41KB (+105B +3.1%) -
@solana/kit production bundle
kit/dist/index.production.min.js
36.09KB (+88B +0.24%) -
Unchanged files (123)
Status Path Size Limits
rpc-graphql/dist/index.browser.mjs
18.82KB -
rpc-graphql/dist/index.native.mjs
18.81KB -
rpc-graphql/dist/index.node.mjs
18.81KB -
transaction-messages/dist/index.browser.mjs
7.25KB -
transaction-messages/dist/index.native.mjs
7.25KB -
transaction-messages/dist/index.node.mjs
7.25KB -
codecs-data-structures/dist/index.browser.mjs
4.69KB -
codecs-data-structures/dist/index.native.mjs
4.69KB -
codecs-data-structures/dist/index.node.mjs
4.69KB -
webcrypto-ed25519-polyfill/dist/index.node.mj
s
3.6KB -
webcrypto-ed25519-polyfill/dist/index.browser
.mjs
3.59KB -
webcrypto-ed25519-polyfill/dist/index.native.
mjs
3.57KB -
instruction-plans/dist/index.browser.mjs
3.42KB -
instruction-plans/dist/index.native.mjs
3.42KB -
instruction-plans/dist/index.node.mjs
3.41KB -
rpc-subscriptions/dist/index.browser.mjs
3.37KB -
rpc-subscriptions/dist/index.node.mjs
3.34KB -
rpc-subscriptions/dist/index.native.mjs
3.31KB -
addresses/dist/index.browser.mjs
2.93KB -
rpc-transformers/dist/index.browser.mjs
2.93KB -
rpc-transformers/dist/index.native.mjs
2.93KB -
addresses/dist/index.native.mjs
2.93KB -
addresses/dist/index.node.mjs
2.93KB -
rpc-transformers/dist/index.node.mjs
2.93KB -
transactions/dist/index.browser.mjs
2.9KB -
transactions/dist/index.native.mjs
2.9KB -
transactions/dist/index.node.mjs
2.9KB -
signers/dist/index.browser.mjs
2.63KB -
signers/dist/index.native.mjs
2.63KB -
signers/dist/index.node.mjs
2.62KB -
codecs-strings/dist/index.browser.mjs
2.53KB -
codecs-strings/dist/index.node.mjs
2.48KB -
codecs-strings/dist/index.native.mjs
2.45KB -
transaction-confirmation/dist/index.node.mjs
2.41KB -
transaction-confirmation/dist/index.native.mj
s
2.36KB -
transaction-confirmation/dist/index.browser.m
js
2.35KB -
sysvars/dist/index.browser.mjs
2.35KB -
sysvars/dist/index.native.mjs
2.34KB -
sysvars/dist/index.node.mjs
2.34KB -
react/dist/index.browser.mjs
2.31KB -
react/dist/index.native.mjs
2.31KB -
react/dist/index.node.mjs
2.31KB -
rpc-subscriptions-spec/dist/index.node.mjs
2.18KB -
rpc-subscriptions-spec/dist/index.native.mjs
2.13KB -
rpc-subscriptions-spec/dist/index.browser.mjs
2.13KB -
keys/dist/index.browser.mjs
2.08KB -
keys/dist/index.native.mjs
2.08KB -
keys/dist/index.node.mjs
2.08KB -
codecs-numbers/dist/index.native.mjs
2.01KB -
codecs-numbers/dist/index.browser.mjs
2.01KB -
codecs-numbers/dist/index.node.mjs
2.01KB -
rpc/dist/index.node.mjs
1.95KB -
rpc-transport-http/dist/index.browser.mjs
1.91KB -
rpc-transport-http/dist/index.native.mjs
1.9KB -
rpc/dist/index.native.mjs
1.8KB -
subscribable/dist/index.node.mjs
1.8KB -
rpc/dist/index.browser.mjs
1.8KB -
subscribable/dist/index.native.mjs
1.75KB -
subscribable/dist/index.browser.mjs
1.74KB -
rpc-transport-http/dist/index.node.mjs
1.72KB -
kit/dist/index.browser.mjs
1.68KB -
kit/dist/index.native.mjs
1.68KB -
kit/dist/index.node.mjs
1.67KB -
rpc-types/dist/index.browser.mjs
1.53KB -
rpc-types/dist/index.native.mjs
1.53KB -
rpc-types/dist/index.node.mjs
1.53KB -
rpc-subscriptions-channel-websocket/dist/inde
x.node.mjs
1.33KB -
rpc-subscriptions-channel-websocket/dist/inde
x.native.mjs
1.27KB -
rpc-subscriptions-channel-websocket/dist/inde
x.browser.mjs
1.26KB -
options/dist/index.browser.mjs
1.18KB -
options/dist/index.native.mjs
1.18KB -
options/dist/index.node.mjs
1.17KB -
accounts/dist/index.browser.mjs
1.13KB -
accounts/dist/index.native.mjs
1.12KB -
accounts/dist/index.node.mjs
1.12KB -
rpc-api/dist/index.browser.mjs
976B -
rpc-api/dist/index.native.mjs
975B -
rpc-api/dist/index.node.mjs
973B -
compat/dist/index.browser.mjs
969B -
compat/dist/index.native.mjs
968B -
compat/dist/index.node.mjs
966B -
rpc-spec-types/dist/index.browser.mjs
962B -
rpc-spec-types/dist/index.native.mjs
961B -
rpc-spec-types/dist/index.node.mjs
959B -
rpc-subscriptions-api/dist/index.native.mjs
870B -
rpc-subscriptions-api/dist/index.node.mjs
869B -
rpc-subscriptions-api/dist/index.browser.mjs
868B -
rpc-spec/dist/index.browser.mjs
852B -
rpc-spec/dist/index.native.mjs
851B -
rpc-spec/dist/index.node.mjs
850B -
promises/dist/index.browser.mjs
799B -
promises/dist/index.native.mjs
798B -
promises/dist/index.node.mjs
797B -
assertions/dist/index.browser.mjs
783B -
instructions/dist/index.browser.mjs
769B -
instructions/dist/index.native.mjs
768B -
instructions/dist/index.node.mjs
767B -
fast-stable-stringify/dist/index.browser.mjs
726B -
fast-stable-stringify/dist/index.native.mjs
725B -
assertions/dist/index.native.mjs
724B -
fast-stable-stringify/dist/index.node.mjs
724B -
assertions/dist/index.node.mjs
723B -
programs/dist/index.browser.mjs
329B -
programs/dist/index.native.mjs
327B -
programs/dist/index.node.mjs
325B -
event-target-impl/dist/index.node.mjs
230B -
functional/dist/index.browser.mjs
154B -
functional/dist/index.native.mjs
152B -
text-encoding-impl/dist/index.native.mjs
152B -
functional/dist/index.node.mjs
151B -
codecs/dist/index.browser.mjs
137B -
codecs/dist/index.native.mjs
136B -
codecs/dist/index.node.mjs
134B -
event-target-impl/dist/index.browser.mjs
133B -
ws-impl/dist/index.node.mjs
131B -
text-encoding-impl/dist/index.browser.mjs
122B -
text-encoding-impl/dist/index.node.mjs
119B -
ws-impl/dist/index.browser.mjs
113B -
crypto-impl/dist/index.node.mjs
111B -
crypto-impl/dist/index.browser.mjs
109B -
rpc-parsed-types/dist/index.browser.mjs
66B -
rpc-parsed-types/dist/index.native.mjs
65B -
rpc-parsed-types/dist/index.node.mjs
63B -

Total files change +1.03KB +0.29%

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

@mcintyre94 mcintyre94 marked this pull request as ready for review October 6, 2025 09:18
@mcintyre94 mcintyre94 force-pushed the assert-uses-entire branch 2 times, most recently from 4bab6b3 to 9529670 Compare October 6, 2025 09:47
@github-actions
Copy link
Contributor

github-actions bot commented Oct 6, 2025

Documentation Preview: https://kit-docs-by9igyds3-anza-tech.vercel.app

Copy link
Collaborator

@steveluscher steveluscher left a comment

Choose a reason for hiding this comment

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

Added via Giphy

* decoder.decode(new Uint8Array([0, 0, 0, 0]), 1); // throws
* ```
*/
export function createDecoderThatUsesExactByteArray<T>(decoder: Decoder<T>): Decoder<T> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

ConsumesEntire?

¯\_(°ペ)_/¯

Copy link
Member Author

Choose a reason for hiding this comment

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

The reason I switched from Entire to Exact was because I started writing tests for things like "this decoder is going to read 4 bytes, and you give it 3 bytes, this gives you a useful error". So I was checking too long and too short, which Exact felt like a better fit for.

But I think this was the wrong approach because eg. our number codecs already error when the input bytes are too short, and I think it probably makes sense to leave that as the responsibility for the codec and just cover the case where the input bytes are too long here - which we don't want codecs to check by default.

I'll change this to just check for a byte array being too long, which I think your other suggestions fit better with too.

Comment on lines 38 to 40
bytesLength: bytes.length,
offsetAfterDecoding: newOffset,
offsetBeforeDecoding: offset,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This makes people do math. Just expectedBytes/actualBytes or just numExcessBytes?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep agreed. Again this was because I wanted to cover the case where you provide too few bytes, and figured the offset before/after is the most helpful information there. But I can remove that case and provide a more helpful error that just considers the bytes being too long.

[SOLANA_ERROR__CODECS__UNION_VARIANT_OUT_OF_RANGE]:
'Union variant out of range. Expected an index between $minRange and $maxRange, got $variant.',
[SOLANA_ERROR__CODECS__BYTES_LENGTH_DECODER_MISMATCH]:
'Byte array of length `$bytesLength` was not decoded to the last byte by the provided decoder. End offset was `$offsetAfterDecoding`.',
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe something with a suggestion of what to do, more like:

This decoder expected a byte array of exactly $expectedLength bytes, but $numExcessBytes unexpected excess bytes remained after decoding. Are you sure that you have chosen the correct decoder for this data?

* ```
*/
export function createDecoderThatUsesExactByteArray<T>(decoder: Decoder<T>): Decoder<T> {
return transformDecoder(decoder, (value, bytes, offset, newOffset) => {
Copy link
Member

Choose a reason for hiding this comment

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

Since transformDecoder is just:

return createDecoder({
    ...decoder,
    read: (bytes, offset) => {
        // Some mapping.
    },
});

Could we not avoid the extra argument to transformDecoder and simply use createDecoder for this helper?

Copy link
Member Author

Choose a reason for hiding this comment

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

That makes sense to me and should make this PR simpler, will refactor. Thanks for the pointer!

@mcintyre94
Copy link
Member Author

Thanks both for the review! I've made a bunch of changes:

  • It now only checks if the offset is before the end of the byte array, instead of also throwing if there are too few bytes. The latter makes sense to keep as the responsibility of individual decoders, and restricting to only the first case makes the error message much more useful
  • It no longer changes the interface of transformDecoder

Copy link
Member

@lorisleiva lorisleiva left a comment

Choose a reason for hiding this comment

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

Very nice! 👌

Copy link
Collaborator

@steveluscher steveluscher left a comment

Choose a reason for hiding this comment

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

Added via Giphy

@mcintyre94 mcintyre94 merged commit 22f18d0 into main Oct 8, 2025
14 checks passed
@mcintyre94 mcintyre94 deleted the assert-uses-entire branch October 8, 2025 09:52
@github-actions
Copy link
Contributor

github-actions bot commented Oct 8, 2025

🔎💬 Inkeep AI search and chat service is syncing content for source 'Solana Kit Docs'

@github-actions
Copy link
Contributor

Because there has been no activity on this PR for 14 days since it was merged, it has been automatically locked. Please open a new issue if it requires a follow up.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 23, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature idea: Add a safer decode function

4 participants