Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: note plaintext size generalization #2

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

PaulLaux
Copy link

@PaulLaux PaulLaux commented Apr 30, 2024

(updated description 20.08.2024)
This PR continues the discussion from: zcash/librustzcash#746. Some things have changed, and we need an updated review to continue.

In order to support note encryption for ZSA, we suggest extending the current zcash_note_encryption implementation. Currently, the COMPACT_NOTE_SIZE is a constant; however, we need to support variable note sizes to include the AssetId field for ZSA notes.

Current state in zcash_note_encryption:

/// The size of a compact note.
pub const COMPACT_NOTE_SIZE: usize = 1 + // version
    11 + // diversifier
    8  + // value
    32; // rseed (or rcm prior to ZIP 212)
/// The size of [`NotePlaintextBytes`].
pub const NOTE_PLAINTEXT_SIZE: usize = COMPACT_NOTE_SIZE + 512;

and

pub const ENC_CIPHERTEXT_SIZE: usize = NOTE_PLAINTEXT_SIZE + AEAD_TAG_SIZE;

Proposed changes:

We suggest converting these constants into new abstract types within the Domain trait: NotePlaintextBytes, NoteCiphertextBytes, CompactNotePlaintextBytes, and CompactNoteCiphertextBytes. These types would then be implemented in the orchard and sapling-crypto crates.

After the discussion of the first version of this PR, the following methods are also proposed to be added to the Domain trait to safely convert byte slices into these new types: parse_note_plaintext_bytes, parse_note_ciphertext_bytes, and parse_compact_note_plaintext_bytes.

Updated Domain trait:

pub trait Domain {
    type EphemeralSecretKey: ConstantTimeEq;
    type EphemeralPublicKey;
    type PreparedEphemeralPublicKey;
    type SharedSecret;
    type SymmetricKey: AsRef<[u8]>;
    type Note;
    type Recipient;
    type DiversifiedTransmissionKey;
    type IncomingViewingKey;
    type OutgoingViewingKey;
    type ValueCommitment;
    type ExtractedCommitment;
    type ExtractedCommitmentBytes: Eq + for<'a> From<&'a Self::ExtractedCommitment>;
    type Memo;

    // Types for variable note size handling:
    type NotePlaintextBytes: NoteBytes;
    type NoteCiphertextBytes: NoteBytes;
    type CompactNotePlaintextBytes: NoteBytes;
    type CompactNoteCiphertextBytes: NoteBytes;

    // New parsing methods for safe conversions:
    fn parse_note_plaintext_bytes(plaintext: &[u8]) -> Option<Self::NotePlaintextBytes>;
    fn parse_note_ciphertext_bytes(output: &[u8], tag: [u8; AEAD_TAG_SIZE]) -> Option<Self::NoteCiphertextBytes>;
    fn parse_compact_note_plaintext_bytes(plaintext: &[u8]) -> Option<Self::CompactNotePlaintextBytes>;

Here, NoteBytes is a helper trait designed to simplify and unify the definition and implementation of these new associated types.

Additionally, constants will be removed from function signatures since they are not known at compilation time. For example:

pub fn try_note_decryption<D: Domain, Output: ShieldedOutput<D, ENC_CIPHERTEXT_SIZE>>(...)

will be replaced with:

pub fn try_note_decryption<D: Domain, Output: ShieldedOutput<D>>(...)

Implementation:

We have provided our initial implementation, complemented by the appropriate changes in the orchard and sapling-crypto crates. See the following modules for details:

https://github.com/QED-it/orchard/blob/zsa1/src/note_encryption/domain.rs
https://github.com/QED-it/sapling-crypto/blob/zsa1/src/note_encryption.rs

Additionally, we made several minor updates in the zcash_primitives crate of QED-it's fork of the librustzcash repository to align it with the described changes in zcash_note_encryption, orchard, and sapling-crypto crates:

https://github.com/QED-it/librustzcash/tree/zsa1/zcash_primitives

dmidem and others added 2 commits December 19, 2023 20:08
* Copy updated src folder from librustzcash/zsa1

* Cherry-pick the latest update of zcash_note_encryption from zcash/librustzcash (it was missed in QED-it/librustzcash)

* Upgrade Rust version

* Upgrade Rust version to 1.65

* Add --features=alloc to command line for build-nodefault jon in ci.yml

* Downgrade Rust version back to 1.61.0

---------

Co-authored-by: Dmitry Demin <[email protected]>
@PaulLaux
Copy link
Author

Regarding the compile time vs runtime discussion, we currently set the array size during compile time in the implementation of the trait: https://github.com/QED-it/orchard/blob/a9af64c146bef38d7ac957983bc78c8a3f54f76e/src/note_encryption/note_encryption_zsa.rs#L12

impl OrchardDomain for OrchardZSA {
    const COMPACT_NOTE_SIZE: usize = COMPACT_NOTE_SIZE_ZSA;

    type NotePlaintextBytes = NoteBytes<{ Self::NOTE_PLAINTEXT_SIZE }>;
    type NoteCiphertextBytes = NoteBytes<{ Self::ENC_CIPHERTEXT_SIZE }>;
    type CompactNotePlaintextBytes = NoteBytes<{ Self::COMPACT_NOTE_SIZE }>;
    type CompactNoteCiphertextBytes = NoteBytes<{ Self::COMPACT_NOTE_SIZE }>;
...

dmidem and others added 2 commits May 16, 2024 16:55
…on (#6)

* Update CHANGELOG.md with changes for note plaintext size generalization

* Improve the description in CHANGELOG.md

---------

Co-authored-by: Dmitry Demin <[email protected]>
Co-authored-by: Dmitry Demin <[email protected]>
src/lib.rs Outdated
Comment on lines 141 to 144
type NotePlaintextBytes: AsMut<[u8]> + for<'a> From<&'a [u8]>;
type NoteCiphertextBytes: AsMut<[u8]> + for<'a> From<(&'a [u8], &'a [u8])>;
type CompactNotePlaintextBytes: AsMut<[u8]> + for<'a> From<&'a [u8]>;
type CompactNoteCiphertextBytes: AsRef<[u8]>;
Copy link
Contributor

@str4d str4d Jun 25, 2024

Choose a reason for hiding this comment

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

It is incorrect to have From bounds on these types, as the conversions are necessarily fallible (the slices can be any length, and the types cannot) and this would force panics in the public API (not just in the internal trait API that we control both sides of). Additionally, NoteCiphertextBytes should not take the tag as a slice (we know its length).

Replace all of these with trait methods that "parse" the slices to get these, e.g. fn parse_note_plaintext_bytes(plaintext: &[u8]) -> Option<Self::NotePlaintextBytes>.

Copy link

@dmidem dmidem Aug 19, 2024

Choose a reason for hiding this comment

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

Done: added three new methods to Domain trait to parse note bytes.

fn parse_note_plaintext_bytes(plaintext: &[u8]) -> Option<Self::NotePlaintextBytes>
fn parse_note_ciphertext_bytes(output: &[u8], tag: [u8; AEAD_TAG_SIZE]) -> Option<Self::NoteCiphertextBytes>
fn parse_compact_note_plaintext_bytes(plaintext: &[u8]) -> Option<Self::CompactNotePlaintextBytes>

Copy link
Contributor

@str4d str4d left a comment

Choose a reason for hiding this comment

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

Reviewed b8bd2a1.

src/lib.rs Outdated
Comment on lines 333 to 334
/// Exposes the note ciphertext of the output. Returns `None` if the output is compact.
fn enc_ciphertext(&self) -> Option<D::NoteCiphertextBytes>;
Copy link
Contributor

Choose a reason for hiding this comment

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

This no longer "exposes" the output, and instead forcibly clones it, which is undesirable in usages of ShieldedOutput outside of zcash_note_encryption. We should instead return a reference that the caller can clone if desired (by adding a Clone bound on `Domain::NoteCiphertextBytes):

Suggested change
/// Exposes the note ciphertext of the output. Returns `None` if the output is compact.
fn enc_ciphertext(&self) -> Option<D::NoteCiphertextBytes>;
/// Exposes the note ciphertext of the output.
///
/// Returns `None` if the output is only compact.
fn enc_ciphertext(&self) -> Option<&D::NoteCiphertextBytes>;

Also, document this change to the trait in the changelog.

Copy link

@dmidem dmidem Aug 6, 2024

Choose a reason for hiding this comment

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

We tried to change the code to return references but encountered a few technical challenges due to how NoteCiphertextBytes and CompactNoteCiphertextBytes are implemented in both the orchard and sapling-crypto crates. These types are defined and implemented as newtypes over fixed-size byte arrays:

type NoteCiphertextBytes = NoteBytesData<{ ENC_CIPHERTEXT_SIZE }>;
type CompactNoteCiphertextBytes = NoteBytesData<{ COMPACT_NOTE_SIZE }>;

pub struct NoteBytesData<const N: usize>(pub [u8; N]);

The issue is that these newtypes wrap arrays that are not directly stored as such in the structs implementing ShieldedOutput. Therefore, we cannot return a reference to them directly because they are constructed on-the-fly from these arrays.

A possible workaround is to change NoteCiphertextBytes and CompactNoteCiphertextBytes type implementations in orchard and sapling-crypto to wrap references instead of arrays, but we would need to adjust the Domain trait to allow lifetimes and define associated types as generic with lifetimes using Rust's Generic Associated Types (GAT):

trait Domain {
    type NoteCiphertextBytes<'a>;
    type CompactNoteCiphertextBytes<'a>;
}

This approach would involve additional changes, impacting all Domain and ShieldedOutput implementations and adding complexity to the codebase. If using references is crucial, we can proceed with this strategy though.

Copy link
Contributor

Choose a reason for hiding this comment

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

After discussion in the ZSA meeting, we will make my suggested change. If an action is non-compact, we know it is possible to get a reference to the appropriate type, and the data is large enough that we don't want to force clones.

Copy link

Choose a reason for hiding this comment

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

Done: now enc_ciphertext returns Option of a reference.

Comment on lines +336 to +337
/// Exposes the compact note ciphertext of the output.
fn enc_ciphertext_compact(&self) -> D::CompactNoteCiphertextBytes;
Copy link
Contributor

Choose a reason for hiding this comment

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

Likewise, we should avoid forcibly cloning here, and instead have a Clone bound on Domain::CompactNoteCiphertextBytes:

Suggested change
/// Exposes the compact note ciphertext of the output.
fn enc_ciphertext_compact(&self) -> D::CompactNoteCiphertextBytes;
/// Exposes the compact view of the output's note ciphertext.
fn enc_ciphertext_compact(&self) -> &D::CompactNoteCiphertextBytes;

Also, document this change to the trait in the changelog.

Copy link
Contributor

Choose a reason for hiding this comment

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

After discussion in the ZSA meeting, we'll leave this method as-is; the issue is that for non-compact actions, we can't get a reference to a fixed-length sub-array. The alternative would be to have a separate CompactShieldedOutput trait (which is effectively what we had in the past), but we can consider that refactor later.

Copy link

Choose a reason for hiding this comment

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

Got it: left enc_ciphertext_compact as-is.

src/lib.rs Outdated
output[NOTE_PLAINTEXT_SIZE..].copy_from_slice(&tag);

output
D::NoteCiphertextBytes::from((output, tag.as_ref()))
Copy link
Contributor

Choose a reason for hiding this comment

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

Once the Froms are replaced, this becomes

Suggested change
D::NoteCiphertextBytes::from((output, tag.as_ref()))
D::parse_note_ciphertext_bytes(output, tag).expect("output is correct length")

the unwrap here being fine because we know that output is derived from D::NotePlaintextBytes, which we can assume is the correct size relative to D::NoteCiphertextBytes.

Non-blocking: I have ideas on how to make this infallible in a type-safe way, but I'll look at that refactor after this PR lands.

Copy link

Choose a reason for hiding this comment

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

Done (added and used parse.. method).

src/lib.rs Outdated
Comment on lines 264 to 267
fn extract_memo(
&self,
plaintext: &Self::NotePlaintextBytes,
) -> (Self::CompactNotePlaintextBytes, Self::Memo);
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, I dislike this change, in part because we want to move away from the notion of compact memo representations in future NUs. But looking at how it is used in this crate, I think it's fine for now if we rename it:

Suggested change
fn extract_memo(
&self,
plaintext: &Self::NotePlaintextBytes,
) -> (Self::CompactNotePlaintextBytes, Self::Memo);
fn split_plaintext_at_memo(
&self,
plaintext: &Self::NotePlaintextBytes,
) -> (Self::CompactNotePlaintextBytes, Self::Memo);

Copy link

Choose a reason for hiding this comment

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

Done: renamed to split_plaintext_at_memo.

src/lib.rs Outdated
) -> Option<(Self::Note, Self::Recipient)>;

/// Extracts the memo field from the given note plaintext.
/// Splits the memo field from the given note plaintext.
Copy link
Contributor

Choose a reason for hiding this comment

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

Paired with the method rename:

Suggested change
/// Splits the memo field from the given note plaintext.
/// Splits the given note plaintext into the compact part (containing the note) and
/// the memo field.

Copy link

Choose a reason for hiding this comment

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

Fixed.

Comment on lines -43 to -49
/// The size of a compact note.
pub const COMPACT_NOTE_SIZE: usize = 1 + // version
11 + // diversifier
8 + // value
32; // rseed (or rcm prior to ZIP 212)
/// The size of [`NotePlaintextBytes`].
pub const NOTE_PLAINTEXT_SIZE: usize = COMPACT_NOTE_SIZE + 512;
Copy link
Contributor

Choose a reason for hiding this comment

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

Either document these removals in the changelog, or (my preference) undo these removals (the constants are still relevant in downstream crates) and alter their docstrings to say that these are pre-ZSA.

Copy link

Choose a reason for hiding this comment

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

Done: documented in the changelog.

Copy link
Author

Choose a reason for hiding this comment

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

These constants applicable to Sapling and Orchard but not OrchardZSA.
We believe keeping the constants here will create a confusion in the future and propose to remove them.
Each implementation should provide these.

Comment on lines -54 to -55
/// The size of an encrypted note plaintext.
pub const ENC_CIPHERTEXT_SIZE: usize = NOTE_PLAINTEXT_SIZE + AEAD_TAG_SIZE;
Copy link
Contributor

Choose a reason for hiding this comment

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

Likewise, either document this removal in the changelog, or (my preference) undo this removal and alter its docstring to say that this is pre-ZSA.

Copy link

Choose a reason for hiding this comment

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

Done: documented in the changelog.

@@ -114,8 +109,6 @@ impl ConstantTimeEq for EphemeralKeyBytes {
}
}

/// Newtype representing the byte encoding of a note plaintext.
pub struct NotePlaintextBytes(pub [u8; NOTE_PLAINTEXT_SIZE]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Document this removal in the changelog.

Copy link

Choose a reason for hiding this comment

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

Done: documented in the changelog.

@@ -192,7 +190,7 @@ pub trait Domain {
fn kdf(secret: Self::SharedSecret, ephemeral_key: &EphemeralKeyBytes) -> Self::SymmetricKey;

/// Encodes the given `Note` and `Memo` as a note plaintext.
fn note_plaintext_bytes(note: &Self::Note, memo: &Self::Memo) -> NotePlaintextBytes;
fn note_plaintext_bytes(note: &Self::Note, memo: &Self::Memo) -> Self::NotePlaintextBytes;
Copy link
Contributor

Choose a reason for hiding this comment

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

Document the trait method changes like this in the changelog.

Copy link

Choose a reason for hiding this comment

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

Done: documented in the changelog.

@@ -403,24 +399,18 @@ impl<D: Domain> NoteEncryption<D> {
}

/// Generates `encCiphertext` for this note.
pub fn encrypt_note_plaintext(&self) -> [u8; ENC_CIPHERTEXT_SIZE] {
pub fn encrypt_note_plaintext(&self) -> D::NoteCiphertextBytes {
Copy link
Contributor

Choose a reason for hiding this comment

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

Document this change in the changelog.

Copy link

Choose a reason for hiding this comment

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

Done: documented in the changelog.

dmidem added a commit to QED-it/zcash_note_encryption that referenced this pull request Jul 25, 2024
dmidem added a commit to QED-it/zcash_note_encryption that referenced this pull request Jul 30, 2024
)

* Attempt to resolve review issues for zcash/pull/2

* NoteBytes moved to zcash_note_encryption (copy of #8) (#9)

* Add NoteBytes

* Implement concat manually for wasm

* Fix slice bounds in concat

* Fmt

* Split interface and implementation of NoteBytes

* Add new method to NoteBytes

---------

Co-authored-by: alexeykoren <[email protected]>

* Resolve review issues for zcash/pull/2 (except returning references from ShieldedOutput methods)

* Fix cargo doc issue

* Fix based on feedback from PR #10 review

* Make split_ciphertext_at_tag a method of ShieldedOutput with minor refactoring

---------

Co-authored-by: Dmitry Demin <[email protected]>
Co-authored-by: alexeykoren <[email protected]>
dmidem and others added 2 commits August 12, 2024 21:16
* Refactor enc_ciphertext to return reference instead of copy

These changes were discussed and suggested in PR zcash_note_encryption#2

* Remove extra spaces in rust-toolchain.toml

* Restore the original order of const definition to reduce PR diff

* Fix the comment for split_plaintext_at_memo

* Fix docstring for NOTE_PLAINTEXT_SIZE

* Update CHANGELOG

* Remove unused constants COMPACT_NOTE_SIZE, NOTE_PLAINTEXT_SIZE, ENC_CIPHERTEXT_SIZE, and update CHANGELOG accordingly

* Minor improvement in CHANGELOG.md

---------

Co-authored-by: Dmitry Demin <[email protected]>
dmidem added a commit to QED-it/orchard that referenced this pull request Aug 14, 2024
…hertext (#112)

This PR updates the `ShieldedOutput` implementation for the
`Action`/`CompactAction` struct to align with the recent changes in the
`zcash_note_encryption` crate. Specifically, the `enc_ciphertext` method
now returns a reference instead of a copy.

This change was discussed and suggested in PR
zcash/zcash_note_encryption#2 review.

---------

Co-authored-by: Dmitry Demin <[email protected]>
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.

3 participants