Skip to content

feat!: ciphertext padding - #630

Open
agureev wants to merge 34 commits into
artem/bundle-actionsfrom
arte/ciphertext-length-padding
Open

feat!: ciphertext padding#630
agureev wants to merge 34 commits into
artem/bundle-actionsfrom
arte/ciphertext-length-padding

Conversation

@agureev

@agureev agureev commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🎯 Purpose

This issue tackles #562. Currently the data size of the encrypted post-state is visible via the ChaCha encryption as it is variable size and unpadded, This PR introduces an opt-in padding mechanism.

The user now supplies an Option<u32> to the PPC specifying to what size does the user want all of the ciphertexts padded. The idea is that if they do not want to pay for storage, they do not need to. If they want to pad, then it is best all notes are padded to the same size so as to not leak in-transaction data. However the circuit itself does not contain exactly how much we pad this is user-supplied.

The wallet behavior is changed to pad everything up to 512 bytes, which is ~200 bytes above the largest AMM program data there is, giving ample room to other applications. The idea is that once an application using a larger data field arrives, the users will simply change the hardcoded size to increase the privacy pool if needed.

⚙️ Approach

  • Make PPC accept a padding size input
  • Add padding size to encryption plaintext
  • Make wallet send in padding size based on set default

🧪 How to Test

cargo test -p lee_core --features host encrypt_p

🔗 Dependencies

#604

🔜 Future Work

Consider whether individual padding is needed or whether a smaller pad ammount is better.

📋 PR Completion Checklist

  • Complete PR description
  • Implement the core functionality
  • Add/update tests
  • Add/update documentation and inline comments

agureev added 30 commits July 13, 2026 21:16
Ciphers submitted currently have no data
BREAKING!

Before: The ciphertext key got generated via using the commmitment and
its index as the key.

After: The ciphertext key replaces the commitment dependence by a
nullifier dependence, still making the key unique by global state. The
nullifier is assumed to be the nullifier of the pre-state of the account.

Mitigation: Use the new decryption algorithm.
BREAKING!

Before: Commitments, nullifier, and ciphertexts of a transaction were
position-aligned. The position revealed which nullifier was hiding a
pre-state to a commitment to which post-state. Similarly with
ciphertexts.

After: The nullifier-cipher connection remains, but now they (and
commitments independently) are shuffled by the byte-order of the
hashes.

Mitigation: Expect the circuit outputs to be jumbled.
BREAKING!

Before: The message format for private transactions included separate
fields for private transaction state identifiers such as commitments
and nullifiers.

After: The commitments/nullifiers/ciphertexts are bound in an Action
struct, likewise for public state-bearing messages.

Mitigation: assume incoming messages are in a newly-specified format.
@github-actions

Copy link
Copy Markdown

Benchmark for d88ed21

Click to view benchmark
Test Base PR %
encryption/decrypt 494.7±2.16ns 494.6±2.40ns -0.02%
encryption/encrypt 658.1±2.56ns 667.2±1.19ns +1.38%
keychain/new_mnemonic 3.5±0.03ms 3.4±0.03ms -2.86%
keychain/new_os_random 3.4±0.02ms 3.4±0.02ms 0.00%
shared_secret_key/sender_encapsulate 40.7±0.50µs 40.5±0.10µs -0.49%

@agureev
agureev force-pushed the arte/ciphertext-length-padding branch from fdc06fe to da15030 Compare July 17, 2026 18:12
@github-actions

Copy link
Copy Markdown

Benchmark for 8ea7f3d

Click to view benchmark
Test Base PR %
encryption/decrypt 550.0±2.05ns 550.8±1.14ns +0.15%
encryption/encrypt 738.8±3.40ns 737.9±4.27ns -0.12%
keychain/new_mnemonic 4.2±0.02ms 4.2±0.02ms 0.00%
keychain/new_os_random 4.2±0.02ms 4.2±0.02ms 0.00%
shared_secret_key/sender_encapsulate 49.5±0.50µs 48.1±0.54µs -2.83%

@agureev
agureev force-pushed the arte/ciphertext-length-padding branch from da15030 to 9a5ee3c Compare July 17, 2026 19:10
@github-actions

Copy link
Copy Markdown

Benchmark for 4a923ec

Click to view benchmark
Test Base PR %
encryption/decrypt 552.0±3.34ns 551.9±6.55ns -0.02%
encryption/encrypt 760.5±3.30ns 725.1±9.47ns -4.65%
keychain/new_mnemonic 4.2±0.02ms 4.1±0.02ms -2.38%
keychain/new_os_random 4.2±0.03ms 4.2±0.06ms 0.00%
shared_secret_key/sender_encapsulate 51.1±0.77µs 48.4±0.13µs -5.28%

agureev added 3 commits July 20, 2026 19:15
BREAKING!

Before: Ciphertexts get generated without padding.

After: The circuit expects maximum padding and pads the plaintext of
the ChaCha cipher accordingly.

Mitigation: Expect possible panics on undervalued padding lengths and
expect larger possible load sized.
@agureev
agureev force-pushed the arte/ciphertext-length-padding branch from 9a5ee3c to dfaf151 Compare July 20, 2026 16:11
@github-actions

Copy link
Copy Markdown

Benchmark for c40bd1a

Click to view benchmark
Test Base PR %
encryption/decrypt 551.2±2.01ns 550.9±1.65ns -0.05%
encryption/encrypt 731.8±2.12ns 728.4±1.60ns -0.46%
keychain/new_mnemonic 4.2±0.01ms 4.1±0.01ms -2.38%
keychain/new_os_random 4.2±0.01ms 4.2±0.01ms 0.00%
shared_secret_key/sender_encapsulate 49.3±0.56µs 50.9±2.79µs +3.25%

@agureev
agureev marked this pull request as ready for review July 20, 2026 19:31
buffer.extend_from_slice(&account.to_bytes());
if let Some(pad_to_len) = pad_to_len {
let pad_to_len = usize::try_from(pad_to_len).expect("pad length fits in usize");
assert!(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Padding closes the length leak nicely. One robustness note: the pad_to_len >= buffer.len() assert runs in-guest, so any account whose plaintext exceeds CIPHERTEXT_PAD_SIZE panics proving and the wallet can't send any private tx for it. At pad 512, HEADER_LEN (81) + 68 overhead leaves ~363 bytes for data, vs DATA_MAX_LENGTH (100 KiB) allowed, so it's latent but could bite on larger payloads. Could we add a host-side check before env write returning a typed error naming the account and overflow, instead of an opaque in-guest panic?

NullifierPublicKey, NullifierSecretKey, SharedSecretKey,
account::{AccountWithMetadata, Nonce},
compute_digest_for_path,
encryption::{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Padding is wired always-on here (Some(CIPHERTEXT_PAD_SIZE) at the one call site), so the issue's opt-out for users who'd rather not pay the storage cost isn't reachable from the wallet, every private tx now writes 7×512 bytes. Reasonable as a default, could you add a line noting the always-on is deliberate, or leave a knob for later?

@moudyellaz moudyellaz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Padding closes the #562 length leak cleanly, but i think that the added tests only assert padded ciphertext length. A padded encrypt then decrypt roundtrip would be worth adding, since the trailing-zero tolerance in Account::from_cursor is what keeps padded notes decryptable and nothing currently pins it. Encrypt with Some(512), decrypt, assert the original kind and account come back.

@agureev agureev added the priority:low Low priority label Jul 29, 2026
@agureev
agureev force-pushed the artem/bundle-actions branch from 9a2c9a2 to 846e194 Compare July 29, 2026 18:54
@agureev agureev self-assigned this Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority:low Low priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants