Skip to content

perf(crypto): one vault read per gift-wrap unwrap, no keypair derive for ECDH - #96

Closed
JSKitty wants to merge 1 commit into
masterfrom
claude/project-thread-ab64rt
Closed

JSKitty wants to merge 1 commit into
masterfrom
claude/project-thread-ab64rt

Conversation

@JSKitty

@JSKitty JSKitty commented Oct 11, 2026

Copy link
Copy Markdown

Requested by JSKitty · project thread

Before: unwrapping one received gift wrap on a local account read the GuardedKey vault twice, once per NIP-44 layer, and derived a full keypair each time just to do ECDH. A vault read is deliberately expensive (4096 to 8191 mix rounds, about 45 to 75 µs depending on the launch), so vault work was roughly 45% of every unwrap.

After: local accounts unwrap with one vault read for both layers, and every GuardedSigner NIP-04/44 op uses the bare SecretKey without deriving a keypair. nip59's checks still run, in the same order: kind, outer verify, seal verify, and rumor author matching the seal.

Numbers

cargo test --release -p vector-core --test bench_unwrap -- --ignored --nocapture, master and this branch run alternately for 8 launches each (4 vCPU Linux):

median range
master UnwrappedGift::from_gift_wrap(GuardedSigner) 373 µs 369–388
this branch GuardedSigner::unwrap_gift_wrap 297 µs (−20%) 265–307

Every NIP-04/44 op through GuardedSigner (seal encrypt on send, NIP-04 paths) also gets ~24 µs cheaper (to_keys()+decrypt vs GuardedSigner nip44_decrypt rows, same binary).

How

  • GuardedKey::to_secret_key(): vault read with no keypair derive. to_keys() now wraps it.
  • GuardedSigner::unwrap_gift_wrap: mirrors nip59::UnwrappedGift::from_gift_wrap_with_ctx, but takes the secret once after the outer verify and drops it straight after the second decrypt.
  • prepare_event uses it for ActiveSigner::Local. Bunker, NIP-55 and raw Keys keep the generic path.
  • Tests: output is identical to nostr's unwrap, and it rejects the same inputs (tampered outer event, wrap for someone else, rumor author not matching the seal, non-gift-wrap). Full cargo test -p vector-core: 1726 passed.

Trade-off to review: the secret now lives for one ~180 µs window covering both decrypts plus the seal parse and verify, instead of two ~60 µs windows. It's still a stack SecretKey, zeroized on drop by the patched fork.

🤖 Generated with Claude Code

https://claude.ai/code/session_01K9tM3F7VFTMcoTN1cmoiuo


Generated by Claude Code

…for ECDH

Every GuardedKey read runs 4096..8191 mix rounds by design (~45-75 µs), and
the generic nip59 unwrap read the vault once per layer, each time also deriving
a keypair it never used. Local accounts now unwrap with a single read and use
the bare SecretKey for NIP-04/44, keeping nip59's checks in the same order.

Inbound unwrap, median of 8 interleaved launches: 373 µs -> 297 µs (-20%).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K9tM3F7VFTMcoTN1cmoiuo
@JSKitty JSKitty self-assigned this Oct 11, 2026
@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

JSKitty commented Oct 11, 2026

Copy link
Copy Markdown
Author

Closing: widescreen already has this (0fea9b9) plus batched unwraps (c0b604b). Future work lands there as local commits instead of PRs.


Generated by Claude Code

@JSKitty JSKitty closed this Oct 11, 2026
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