Skip to content

fix: defensively copy UnsignedByte instances in UnsignedByteArray - #857

Open
sappenin wants to merge 2 commits into
mainfrom
fix/unsigned-byte-array-defensive-copy
Open

sappenin wants to merge 2 commits into
mainfrom
fix/unsigned-byte-array-defensive-copy

Conversation

@sappenin

Copy link
Copy Markdown
Collaborator

Summary

UnsignedByte is mutable (destroy() zeroes its value), but UnsignedByteArray stored caller-supplied UnsignedByte instances by reference. Several call sites build arrays from shared static constants, so destroying one array mutated process-wide state:

  • PrivateKey.prefixedBytes() placed the shared PrivateKey.ED2559_PREFIX / SECP256K1_PREFIX constant at index 0. Calling destroy() on the exported array set ED2559_PREFIX to 0x00 for the whole JVM. After that, fresh ed25519 exports carried a 00 prefix, fromPrefixedBytes rejected genuine ED-prefixed keys, and 00-prefixed input was classified as ED25519 because both constants compared equal.
  • BcKeyUtils.toPublicKey(Ed25519PublicKeyParameters) stored PublicKey.ED2559_PREFIX inside the resulting PublicKey.value(), with the same exposure.
  • IssueType appended the static ACCOUNT_ONE array into every MPT issue serialization via append(UnsignedByteArray), which used addAll without copying.
  • slice() returned a subList view, so destroying a slice wiped the parent array.

Fix

Fixed at the UnsignedByteArray level so the class never aliases an UnsignedByte it did not allocate:

  • The public constructor deep-copies the list and every element.
  • of(UnsignedByte...), append(UnsignedByte), append(UnsignedByteArray) and set() copy their inputs.
  • slice() and reverse() return independent arrays.
  • Factories that already allocate fresh instances (of(byte[]), fromHex, ofSize, empty) use a private no-copy constructor, so their cost is unchanged.
  • append keeps its documented mutate-and-return-this chaining semantics.

Note: append(UnsignedByteArray) now allocates one UnsignedByte per appended byte on the codec path where it previously shared references.

Tests

Written red-first; 14 failures before the fix, all green after.

  • UnsignedByteArrayTest: one aliasing test per path (constructor, of, both append overloads, set, slice, reverse).
  • PrivateKeyPrefixDestroyTest (new): destroying prefixedBytes() leaves both prefix constants intact and fromPrefixedBytes still classifies correctly.
  • BcKeyUtilsTest: toPublicKey does not share PublicKey.ED2559_PREFIX; destroying its value leaves the constant intact.
  • IssueTypeTest: destroying an MPT issue's bytes does not change the next serialization.
  • SeedTest: derived ed25519 public key does not share the prefix.
  • UnsignedByteTestUtils (new, test-only): reflection reset of shared constants, used as a safety net so a future regression cannot poison the rest of the fork.

Full xrpl4j-core unit suite and checkstyle pass.

🤖 Generated with Claude Code

UnsignedByte is mutable (destroy() zeroes it), but UnsignedByteArray
stored caller-supplied instances by reference. PrivateKey.prefixedBytes()
and BcKeyUtils.toPublicKey() built arrays whose first element was the
shared static ED2559_PREFIX / SECP256K1_PREFIX constant, so destroying
an exported key zeroed the process-wide prefix: later ed25519 exports
carried a 0x00 prefix and fromPrefixedBytes misclassified keys. IssueType
shared the static ACCOUNT_ONE bytes the same way, and slice() returned a
subList view whose destroy() wiped the parent.

UnsignedByteArray now copies every UnsignedByte it receives via the
public constructor, of(UnsignedByte...), append(UnsignedByte),
append(UnsignedByteArray) and set(), and returns independent arrays from
slice() and reverse(). Factories that already allocate fresh instances
use a private no-copy constructor so of(byte[]), fromHex, ofSize and
empty are unchanged in cost.

Adds aliasing tests for each UnsignedByteArray path and regression tests
for PrivateKey, BcKeyUtils, Seed and IssueType, plus a test helper that
restores the shared constants so a regression cannot poison the fork.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between f36db22 and 4ed8aeb.

Files selected for processing: 1
  • xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/UnsignedByteArray.java

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.

Walkthrough

UnsignedByteArray now copies mutable UnsignedByte elements across construction, factory, mutation, slice, and reverse operations. Tests cover source isolation and regression cases for serialization and key-prefix values.

Changes

Unsigned byte ownership

Layer / File(s) Summary
Copy elements across array operations
xrpl4j-core/src/main/java/org/xrpl/xrpl4j/codec/addresses/UnsignedByteArray.java, xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/addresses/UnsignedByteArrayTest.java
Construction, factory, append, set, slice, and reverse operations now use independent UnsignedByte elements. Tests check that destroying a result leaves its inputs and source arrays unchanged.
Serialization and key-prefix regression tests
xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/addresses/UnsignedByteTestUtils.java, xrpl4j-core/src/test/java/org/xrpl/xrpl4j/codec/binary/types/IssueTypeTest.java, xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/keys/PrivateKeyPrefixDestroyTest.java, xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/keys/SeedTest.java, xrpl4j-core/src/test/java/org/xrpl/xrpl4j/crypto/keys/bc/BcKeyUtilsTest.java
Tests check that destroying serialized or key values does not alter shared prefixes or later serialization and conversion results. A test utility resets shared UnsignedByte values.

Priority: Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Merge Risk: Minimal · up to 4ed8a

The ownership changes satisfy the intended isolation behavior, with no established merge-blocking risk.

Pre-merge checks: Passed 5
Check name Status Explanation
Title check Passed The title clearly and concisely identifies the main change: defensive copying of UnsignedByte instances in UnsignedByteArray.
Description check Passed The description directly explains the aliasing defect, the defensive-copying fix, affected call sites, API behavior, and regression tests.
Docstring Coverage Passed Docstring coverage is inconclusive: files from earlier reviews still need to be rechecked within the check's analysis limits. Not blocking.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sappenin sappenin self-assigned this Oct 23, 2026
@sappenin sappenin added the bug Something isn't working label Oct 23, 2026
@sappenin sappenin added this to the v7.0.0 milestone Oct 23, 2026
@sappenin
sappenin requested a review from kuan121 October 23, 2026 23:29

@xrplf-ai-reviewer xrplf-ai-reviewer 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.

No issues found.

@codecov

codecov Bot commented Oct 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

The private non-copying constructor of UnsignedByteArray used an unused
`boolean ignored` parameter purely to disambiguate it from the public
copying constructor. Replace it with a private `Ownership` enum so call
sites read as `new UnsignedByteArray(list, Ownership.TAKE)`, making the
take-ownership intent explicit and silencing the unused-parameter
warning.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@xrplf-ai-reviewer xrplf-ai-reviewer 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.

No issues found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant