Skip to content

fix(sdk-coin-ada): stop using cbor's sync encode statics for CIP8 structures - #10089

Merged
nayandas190 merged 1 commit into
masterfrom
nayanjyotidas/cecho-2590-ada-cip8-cbor-fix
Oct 29, 2026
Merged

nayandas190 merged 1 commit into
masterfrom
nayanjyotidas/cecho-2590-ada-cip8-cbor-fix

Conversation

@nayandas190

Copy link
Copy Markdown
Contributor

Description

@bitgo/sdk-coin-ada builds its CIP8 / COSE payloads with cbor's synchronous statics (Encoder.encode / encodeOne / encodeCanonical) in modules/sdk-coin-ada/src/lib/messages/cip8/utils.ts. Internally those funnel through Encoder._encodeAll, which returns a single read() chunk; on Node.js 26 the encoder emits its output in 1-byte chunks, so the encoding was silently truncated to one byte and any decode failed with unexpected end of input.

  • Replace those calls with a local encodeCbor helper that collects the encoder's data events. Attaching data puts the Transform in flowing mode, so chunks are released synchronously while the encoder writes.
  • Add a regression test that round-trips a payload larger than the cbor encoder chunk size (64 KB), and a comment on encodeCbor documenting why draining buffered output with read() is not an option (past the high-water mark push() returns false and cbor's chunked push() chains short-circuit, truncating large values).

Minimal repro of the underlying bug:

require('cbor').encode({ a: 1, b: 'x' }).toString('hex')
// node v24.15.0 -> a261610161626178  (8 bytes)
// node v26.12.1 -> a2                (1 byte)

Split out of #10088 (statics bot-token change) so it can land independently: Node.js 26 is a required CI matrix entry since #10043 (WCN-3753), and unit-test-changed runs changed packages plus their dependents — @bitgo/sdk-coin-ada depends on @bitgo/statics, which is why the 26.x job in #10088 surfaced this.

Issue Number

CECHO-2590

Type of change

  • Bug fix (non-breaking change which fixes an issue)

How Has This Been Tested?

  • yarn --cwd modules/sdk-coin-ada unit-test on Node 22.23.2, 24.15.0 and 26.12.1 — 195 passing, exit 0 (was 192 passing / 2 failing on Node 26 before the fix)
  • CIP8 -> COSE -> decode round-trip smoke test for 15 B / 100 KB / 1 MB messages on all three versions
  • The new 64 KB regression test also fails under the pushAny + drain read() alternative, which is why that approach was not used

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • My code compiles correctly for both Node and Browser environments
  • My commits follow Conventional Commits and I have properly described any BREAKING CHANGES
  • The ticket or github issue was included in the commit message as a reference
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

…uctures

`cbor`'s synchronous statics (`Encoder.encode`/`encodeOne`/`encodeCanonical`) only return the
first chunk read from the encoder stream, so on Node.js 26 the encoded bytes are truncated to one
byte and any subsequent decode fails with "unexpected end of input". That broke the CIP8 utils
unit tests and the COSE encode/decode helpers as soon as Node.js 26 became a required CI matrix
entry (WCN-3753).

Encode by collecting the encoder's `data` events instead: that puts the stream in flowing mode, so
chunks are released synchronously as the encoder writes. Draining buffered output with `read()`
does not work — past the readable high-water mark `push()` returns false and the encoder's chunked
`push()` chains short-circuit, truncating large values; the regression test added here (a 64 KB
CIP8 payload, i.e. encoding above the encoder chunk size) fails under that implementation on Node
22.23.2, 24.15.0 and 26.12.1.

Verified locally: @bitgo/sdk-coin-ada unit tests 195 passing (exit 0) on Node 22.23.2, 24.15.0 and
Node 26.12.1, plus a COSE round-trip smoke test for messages up to 1 MB on all three versions.

Ticket: CECHO-2580
@linear-code

linear-code Bot commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

CECHO-2590

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Compatibility checks are failing on Node.js 27-nightly (experimental, non-blocking). This does not block merge, but it signals an incompatibility to fix before this release becomes supported.

View run

@nayandas190
nayandas190 marked this pull request as ready for review October 29, 2026 05:08
@nayandas190
nayandas190 requested a review from a team as a code owner October 29, 2026 05:08

@claude claude Bot 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.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@nayandas190
nayandas190 merged commit 7243e52 into master Oct 29, 2026
27 checks passed
@nayandas190

Copy link
Copy Markdown
Contributor Author

@claude please review.

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @nayandas190's task in 27s —— View job


Review of #10089 (already merged)

Verdict: looks good. I have one thing to confirm and a few minor notes.

The fix is sound. encodeCbor in modules/sdk-coin-ada/src/lib/messages/cip8/utils.ts attaches a data listener, which puts the stream in flowing mode. It then calls write and end, and joins the chunks with Buffer.concat. That avoids the single-read() truncation in cbor's sync statics on Node 26. The doc comment explains why draining with read() doesn't work, and that's useful for the next person to touch this.

To confirm

  • Canonical encoding: the PR description says the old code used encodeCanonical as well as encode and encodeOne. new Encoder() is non-canonical by default, so it won't sort map keys or pick canonical forms. encodeCanonical is new Encoder({canonical: true}) in cbor, so the options differ. I couldn't see the pre-PR file (the clone is shallow), so I couldn't check which call site used it.
    • If a call site used encodeCanonical and relied on key ordering, output bytes would change. For example, sigStructureCborBytes is what gets signed.
    • The protected header map is built in a fixed order (1, then 'address'), so it's probably unaffected.
    • The existing tests still pass, so this is probably fine. If any site used encodeCanonical, encodeCbor could take an options argument, like new Encoder({ canonical: true }), to preserve the old behavior.

Minor notes

  • Error handling: encodeCbor has no 'error' listener. If the encoder hits an unsupported value, the error is emitted with no listener and rethrown, which should surface as a synchronous throw. That matches the old behavior closely enough. Capturing the error and throwing it explicitly after end() would make this clearer.
  • Regression test: the 64 KB test checks the round-trip through the hex fields only. It would be stronger to also assert that buffer.length is greater than 64 KB, so it fails clearly if the payload were ever dropped silently. The comment in the test already describes this intent.
  • Node 27-nightly: the compatibility check failing on 27-nightly is flagged as non-blocking. I haven't checked whether it's related. If it is, the cause is probably a similar stream or chunking change, and this helper is the place to look.

I only read the code and haven't run the tests, so the results in the PR description are unverified by me.

@nayandas190
nayandas190 deleted the nayanjyotidas/cecho-2590-ada-cip8-cbor-fix branch October 29, 2026 05:28
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