Skip to content

perf: build note plaintexts in a stack buffer instead of a heap Vec#8

Merged
QuantumExplorer merged 2 commits into
dashifiedfrom
perf/stack-note-plaintext
Jul 2, 2026
Merged

perf: build note plaintexts in a stack buffer instead of a heap Vec#8
QuantumExplorer merged 2 commits into
dashifiedfrom
perf/stack-note-plaintext

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

Fixes finding #4 from the MemoSize PR review: Domain::note_plaintext_bytes assembled the secret note plaintext (diversifier, value, rseed, memo) in a 52-byte stack array, copied it into a transient alloc::vec! heap buffer, and copied that again via from_slice — one heap allocation plus two extra copies per encrypted output on a path that was allocation-free before the memo generalization. The Vec was also dropped without zeroization, making it the only heap copy of note plaintext anywhere in the encryption path.

Changes

  • note_plaintext_bytes now writes the plaintext fields directly into a single max-size stack buffer ([u8; COMPACT_NOTE_SIZE + MAX_MEMO_SIZE] = 564 bytes) and converts once with from_slice(&buf[..len]): zero heap allocations, one copy instead of three buffers
  • New pub const MAX_MEMO_SIZE: usize = 512 in memo.rs, with a corresponding bound added to MemoSize::SIZE_CHECK — the compiler now guarantees the stack buffer is large enough for every M that compiles, so the change introduces no new panic path
  • Secret data no longer transits an unzeroized heap allocation (stack-only, matching the pre-feat: make memo size generic via MemoSize trait #1 behavior)

Compatibility

  • Wire format: byte-for-byte unchanged, pinned by the existing note encryption test vectors and the golden ciphertext/txid vectors added in feat: enforce MemoSize invariants at compile time and add DashMemo coverage #7 (all pass unmodified)
  • The MEMO_SIZE <= 512 bound is a new compile-time constraint on third-party MemoSize implementations; both provided impls (512/36) are unaffected, and 512 is the largest known protocol memo. A future larger-memo protocol would bump one constant

Test plan

  • cargo test — 87 tests pass (full suite including proving tests; golden vectors unchanged)
  • cargo clippy --all-features --all-targets clean
  • cargo fmt -- --check
  • RUSTDOCFLAGS="-D warnings" cargo doc --all-features --document-private-items clean

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved memo handling for notes, with stricter size checks that reject oversized memos earlier.
    • Switched note plaintext assembly to a fixed-size buffer, helping reduce allocation overhead and improve reliability.

note_plaintext_bytes assembled the secret note plaintext (diversifier,
value, rseed, memo) in a 52-byte stack array, copied it into a
transient alloc::vec! buffer, and copied that again via from_slice --
one heap allocation plus two redundant copies per encrypted output on
a previously allocation-free path, with the Vec freed without
zeroization leaving the only heap copy of note plaintext in the
encryption path.

Build the plaintext directly in a single max-size stack buffer
(COMPACT_NOTE_SIZE + MAX_MEMO_SIZE = 564 bytes) and convert once with
from_slice(&buf[..len]). A new MAX_MEMO_SIZE bound in SIZE_CHECK
guarantees at compile time that the buffer is large enough for any
MemoSize implementation that compiles.

Output is byte-for-byte unchanged, as pinned by the note encryption
test vectors and the golden vectors added in #7.

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

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@QuantumExplorer, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 29 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 46292276-c751-4652-b083-84c9a30ed0f5

📥 Commits

Reviewing files that changed from the base of the PR and between b348ee2 and dc0ea01.

📒 Files selected for processing (1)
  • src/note_encryption.rs
📝 Walkthrough

Walkthrough

Adds a MAX_MEMO_SIZE constant (512 bytes) with a compile-time assertion in MemoSize::SIZE_CHECK, and changes note plaintext construction in note_encryption.rs to use a fixed-size stack buffer (COMPACT_NOTE_SIZE + MAX_MEMO_SIZE) instead of a heap-allocated Vec.

Changes

Memo size bound and plaintext buffer

Layer / File(s) Summary
MAX_MEMO_SIZE constant and compile-time check
src/memo.rs
Adds MAX_MEMO_SIZE (512) constant with documentation and extends MemoSize::SIZE_CHECK to assert MEMO_SIZE <= MAX_MEMO_SIZE.
Stack-buffer note plaintext assembly
src/note_encryption.rs
Imports MAX_MEMO_SIZE and rewrites note_plaintext_bytes to write compact fields and memo bytes into a fixed-size stack buffer, then converts the slice via NotePlaintextBytes::from_slice.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • dashpay/orchard#1: Introduces the same MAX_MEMO_SIZE constant, compile-time bound check, and fixed stack buffer approach for note plaintext assembly.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: moving note plaintext construction from a heap Vec to a stack buffer for performance.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/stack-note-plaintext

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/note_encryption.rs`:
- Around line 205-224: The memo handling in the `encrypt`/plaintext assembly
logic needs a runtime length check because `M::SIZE_CHECK` only validates the
type size, not the length returned by `AsRef<[u8]>`. In the `note_encryption`
path, assert that `memo.as_ref().len() == M::MEMO_SIZE` before copying, and
compute the final `len` using `M::MEMO_SIZE` rather than the dynamic slice
length so the stack buffer copy and `NotePlaintextBytes::from_slice` stay
consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 54b4afd2-8706-4f7f-bc27-c39a4e182885

📥 Commits

Reviewing files that changed from the base of the PR and between 6d3bc20 and b348ee2.

📒 Files selected for processing (2)
  • src/memo.rs
  • src/note_encryption.rs

Comment thread src/note_encryption.rs
SIZE_CHECK constrains the size of the Memo type, but a custom AsRef
implementation could still return a slice of a different length,
surfacing as an opaque slice-index panic in the buffer copy. Assert
the slice length explicitly and derive the plaintext length from
M::MEMO_SIZE, which SIZE_CHECK already bounds by MAX_MEMO_SIZE.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@QuantumExplorer QuantumExplorer merged commit eda1309 into dashified Jul 2, 2026
18 checks passed
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.

1 participant