Skip to content

Upstream PR 471 (OrchardZSA) – internal review and fixes#200

Closed
dmidem wants to merge 156 commits into
zcash-ltf-zsafrom
zcash-pr471-review-fixes
Closed

Upstream PR 471 (OrchardZSA) – internal review and fixes#200
dmidem wants to merge 156 commits into
zcash-ltf-zsafrom
zcash-pr471-review-fixes

Conversation

@dmidem
Copy link
Copy Markdown

@dmidem dmidem commented Dec 7, 2025

This PR mirrors zcash#471 (QED-it:zsa1 → zcash:ltf/zsa) on our fork for internal review and changes.

We’ll address upstream review comments here, then propagate the final commits and responses back to zcash#471.

PaulLaux and others added 30 commits December 6, 2022 15:02
* Added .circleci/config.yml
Implements the issuer keys as

    IssuerAuthorizingKey -> isk
    IssuerVerifyingKey -> ik

Test vectors generated with zcash_test_vectors repo
* Added NoteType to Notes
* Added NoteType to value commitment derivation
* Circleci project setup (#1)

* Added .circleci/config.yml

* Added NoteType to Notes

* reformated file

* updated `derive` for NoteType

* added note_type to value commit derivation

* rustfmt

* updated ci config

* updated ci config

* updated ci config

* updated derive for note_type

* added test for arb note_type

* added test for `native` note type

* zsa-note-encryption: introduce AssetType and encode and decode it in note plaintexts

* zsa-note-encryption: extend the size of compact notes to include asset_type

* fixed clippy warrnings

* rustfmt

* zsa-note-encryption: document parsing requirement

* zsa-note-encryption: revert support of ZSA compact action

* zsa_value: add NoteType method is_native

* zsa-note-encryption: remove dependency on changes in the other crate

* zsa-note-encryption: extract memo of ZSA notes

* zsa-note-encryption: tests (zcash_test_vectors 77c73492)

* zsa-note-encryption: simplify roundtrip test

* zsa-note-encryption: more test vectors (zcash_test_vectors c10da464)

* Circleci project setup (#1)

* Added .circleci/config.yml

* issuer keys implementation (#5)

Implements the issuer keys as

    IssuerAuthorizingKey -> isk
    IssuerVerifyingKey -> ik

Test vectors generated with zcash_test_vectors repo

* Added NoteType to Notes (#2)

* Added NoteType to Notes
* Added NoteType to value commitment derivation

* zsa-note-encryption: use both native and ZSA in proptests

* zsa-note-encryption: test vector commit 51398c93

* zsa-note-encryption: fix after merge

Co-authored-by: Paul <3682187+PaulLaux@users.noreply.github.com>
Co-authored-by: Paul <lauxpaul@protonmail.com>
Co-authored-by: Aurélien Nicolas <info@nau.re>
Co-authored-by: Daniel Benarroch <danielbenarroch92@gmail.com>
+ Updated test bsk_consistent_with_bvk to verify mixed note types.
+ Added NoteType support to the builder and the bundle.
+ added split_flag to SpentInfo and as input to the Circuit (currently commented out)
+ added conditional cv_sum calculation (currently commented out)
+ added padding to actions
- added IssueBundle and IssueAction
- added a builder for IssueBundle
- added verify_issue_bundle() for consensus verification.
- unit tests.
added tests in `tests/zsa.rs`
* disabled split notes and proof check for zsa transfer
* fixes and suggestions

* changed "issuer" to "issuance" as per zcash#356 (comment)

* terminology fixes

* updated naming
* rename 2 note_type -> asset as per  zcash#356 (comment)

* added a dedicated type for "IssuanceAuth"

* disabled codecov github action due to bad behavior. 

* extracted "is_asset_desc_of_valid_size()" into asset_id.rs
* improved `verify_issue_bundle()`
Added a method to add assets to burn to the Builder

bvk computation now includes the burnt assets

Added Tests for bsk/bvk consistency for burning

Added E2E tests for assets burning
Added CI badge to README
Added `OrchardDomainV3` on top of the encryption generalization (QED-it/librustzcash#18).

not for review: note_encryption.rs, note_encryptionv2v3.rs and src/test_vectors/note_encryption.rs. These files represent two possible approaches for backward compatibility and will be finalized down the road. (the files were excluded from the build).
When split_flag is set, the following values are modified
* v_net is equal to -v_new instead of v_old - v_new
* cv_net is evaluated with this new value of v_net

The following constraints are modified
* (v_old - v_new = magnitude * sign) becomes (v_old * (1-split_flag) - v_new = magnitude * sign) to take into
   account the new value of v_net
* nf_old = nf_old_pub is only checked when split_flag=0
 * the new constraint asset_old = asset_new is always checked regardless of the value of split_flag
- Renamed AssetId to AssetBase
- Changed the  AssetBase implementation to support the zip update.
- Updated visibility for various members of issuance.rs
…#49)

This PR updates the test-vectors from the updates to the zcash-test-vectors repository (see here).

The keys test is also updated to now use the asset base from the test vectors instead of just using the native asset.
In the circuit, we update value_commit_orchard to take into account asset.
Previously, value_commit_orchard returns cv_net = [v_net] ValueCommitV + [rcv] ValueCommitR..
Now, value_commit_orchard returns cv_net = [v_net] asset + [rcv] ValueCommitR.
ValueCommitV and ValueCommitR are constants
v_net is equal to sign * magnitude where sign is in {-1, 1} and magnitude is an unsigned integer on 64 bits.

To evaluate [v_net] asset where v_net = sign * magnitude, we perform the following steps
1. verify that magnitude is on 64 bits
2. evaluate commitment=[magnitude]asset with the variable-base long-scalar multiplication
3. evaluate result=[sign]commitment with the new mul_sign gate
We would like to have a constant-time evaluation of the note commitment for both ZEC and ZSA.
ZEC_note_commitment=Extract_P(SinsemillaHashToPoint(zec_personalization, common_bits) + [rcm]R)
ZSA_note_commitment=Extract_P(SinsemillaHashToPoint(zsa_personalization, common_bits || asset) + [rcm]R)

R is the same constant for ZEC and ZSA note commitments.
1. Added a new error, `ValueSumOverflow`, that occurs if the sum value overflows when adding new supply amounts.
2. Created a new `supply_info` module containing `SupplyInfo` and `AssetSupply` structures, with `add_supply` function and unit tests for it.
3. Renamed the `are_note_asset_ids_derived_correctly` function to `verify_supply`, changed its behavior to verify and compute asset supply, added unit tests for it.
4. Updated the `verify_issue_bundle` function to use the changes mentioned above, updated its description, and added new unit tests.
5. Renamed errors with `...NoteType` suffix in the name to `...AssetBase`.
6.  Added `update_finalization_set` method to `SupplyInfo` and use after the calls of `verify_issue_bundle function` (if needed), instead of mutating the finalization set inside `verify_issue_bundle`.
- Add getter method for Bundle.burn field
For zcash_note_encryption, we have to use version 0.2 with QEDIT patch.
In the circuit, we update note_commit to take into account asset.
Previously, note_commit returns cm = hash(Q_ZEC, msg) + [rcm]R.
Now, note_commit returns
- cm = hash(Q_ZEC, msg) + [rcm]R for ZEC note
- cm = hash(Q_ZSA, msg || asset) + [rcm]R for ZSA note

We now evaluate note_commit with the following steps
1. evaluate **hash_zec = hash(Q_ZEC, msg)**
2. evaluate **hash_zsa = hash(Q_ZSA, msg || asset)**
3. select **hash = hash_zec if is_native_asset**
                         **= hash_zsa otherwise**
4. evaluate **cm = hash + [rcm]R**
5. check some constraints on msg and asset and their decompositions
6. return **cm**

The following modifications are required to update note_commit:
- add a is_native_asset witness (and check that it is a boolean and its
value is correct according to asset)
- add a MUX chip to evaluate a multiplexer on Pallas points

Warning: we increased the size of the Orchard circuit !
… = nf_old_pub) (#62)

Currently, every new note commitment is calculated using
rho_new = nf_old = DeriveNullifier_nk(rho_old, psi_old, cm_old).
For split notes, we would like to evaluate the new note commitment with
rho_new = nf_old_pub (a random nullifier which is stored in the instance nf_old_pub).
For all remaining notes, nf_old = nf_old_pub.
As such, implementing rho_new = nf_old_pub for all notes will not affect
those remaining notes (and only affect split notes).
Comment thread src/pczt/tx_extractor.rs
|_, Unbound { proof, bsk }| {
Authorized::from_parts(
proof,
VerBindingSig::new(OrchardSighashVersion::V0, bsk.sign(rng, &sighash)),
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): Non-blocking: The sighash version needs to be passed into the method, because it is a property of the sighash (and only the caller knows which version was used). Given that this will need an API refactor across several crates to ensure consistency, let's not do it in this PR (which is complicated enough as it is).

Comment thread src/sighash_versioning.rs
/// No version (used for Orchard and TXv5 compatibility).
/// TXv5 does not require the sighash versioning bytes.
NoVersion = u8::MAX,
}
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): A repr(u8) enum with unique values matching the sighashVersions cannnot work here, because:

  • The v0 sighash for tx v6 will almost certainly be different from the v0 sighash for tx v7.
  • No PCZT encoding has been defined yet.
  • We do not know in the orchard crate what the actual encoding of the sighash version will be; it requires knowledge of the transaction version in which the sighash is used (in addition to the sighash algorithm).
  • Future sighash versions may have associatedData, which cannot be conveyed in an enum like this.

Instead of using version numbers for the enum variants, they should be the semantic digest operation that is expected here; the mapping from that to actual serialized version numbers should happen in zcash_primitives. This also means the NoVersion variant is unnecessary (the difference is handled by the tx serializer).

Suggested change
}
/// The kind of data that a sighash commits to.
///
/// This is used to implement [sighash versioning] for transactions containing Orchard
/// bundles.
///
/// [sighash versioning]: https://zips.z.cash/zip-0246#sighash-versioning
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum OrchardSighashKind {
/// The "default" sighash that commits to all effecting data of the transaction.
///
/// Corresponds to the Orchard parts of the following specifications:
/// - [ZIP 244](https://zips.z.cash/zip-0244#s-4-orchard-digest)
/// - [ZIP 246](https://zips.z.cash/zip-0246#s-4-orchard-digest)
AllEffecting,
}

(Then when a new digest algorithm is defined for a given tx version that commits to a subset of data, we'd add a new variant here representing the commitment to the subset of Orchard; likewise if a bug is found in a given tx version's AllEffecting committed data, we'd add a new variant representing the fixed commitment strategy.)

As a result, this entire module should be rewritten to avoid mentioning "versioning" (other than by motivating reference in documentation like above) as that is a transaction-level concern, not an Orchard bundle-level concern.

Comment thread src/action.rs
/// some note created prior to this action (adding a nullifier to the global ledger).
#[derive(Debug, Clone)]
pub struct Action<A> {
pub struct Action<A, P: OrchardPrimitives> {
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): Document the change to this struct in the changelog.

Comment thread src/action.rs
nf in arb_nullifier(),
rk in arb_spendauth_verification_key(),
note in arb_note(output_value),
asset in arb_asset_base(),
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): This is in the wrong place. The asset for the action should be an argument of arb_unauthorized_action alongside the spend and output values, otherwise the caller cannot leverage those existing arguments in any meaningful way.

Suggested change
asset in arb_asset_base(),

Comment thread src/action.rs
note in arb_note(output_value),
rng_seed in prop::array::uniform32(prop::num::u8::ANY),
fake_sighash in prop::array::uniform32(prop::num::u8::ANY),
asset in arb_asset_base(),
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): Ditto:

Suggested change
asset in arb_asset_base(),

@@ -1,3 +1,5 @@
//! `Add` chip implemetation.
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): ```suggestion
//! Add chip implementation.

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum IssueSighashVersion {
/// Version V0.
V0,
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): I think most of the same rationale applies here as for the Orchard sighash kind, i.e. this should be AllEffecting instead of V0.

I agree that it should be a separate enum from the Orchard sighash kind, at least for the current approach. This would in theory allow a TransactionData to be constructed that claimed to use incompatible OrchardSighashKind and IssueSighashKind values, but that should be detected and rejected at tx serialization time. I'll see if I can think of a cleaner approach...

Comment thread src/note/commitment.rs
///
/// [concretesinsemillacommit]: https://zips.z.cash/protocol/nu5.pdf#concretesinsemillacommit
pub(super) fn derive(
/// [notecommit]: https://zips.z.cash/zip-0226#note-structure-commitment
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): ```suggestion
/// [notecommit]: https://zips.z.cash/zip-0226#note-structure-and-commitment

Comment thread src/note/commitment.rs
/// $NoteCommit^Orchard$.
///
/// Defined in [Zcash Protocol Spec § 5.4.8.4: Sinsemilla commitments][concretesinsemillacommit].
/// Defined in [ZIP-226: Transfer and Burn of Zcash Shielded Assets][notecommit].
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): Do not remove the existing reference; it is the correct reference for $NoteCommit^Orchard$. Instead reference both locations, and also mention $NoteCommit^OrchardZSA$ in the docstring.

Comment thread src/primitives.rs
mod zcash_note_encryption_domain;

pub use {
compact_action::CompactAction, orchard_domain::OrchardDomain,
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): It feels very wrong for a CompactAction to be considered an "Orchard primitive", when it is actually derived from a more fundamental value (Action). This is a misunderstanding of the primitives module, and gets to a complaint I raised with you months ago that I didn't think the deletion of the note_encryption module was the right way to approach the refactor.

Non-blocking because it doesn't affect the correctness of the PR (and the history of this PR is more valuable to me than ease of introspecting the history of the note encryption logic), but be warned that I will likely go through after this PR is merged (and maybe after the corresponding librustzcash PR is merged) and undo a bunch of your refactoring (with the effect of having logically reduced the diff of this PR, even if it doesn't reduce the actual diff that gets merged).

In the meantime:

  • Document these additions to the primitives module in the changelog.

Comment thread src/note/asset_base.rs
impl Ord for AssetBase {
fn cmp(&self, other: &Self) -> Ordering {
let self_coord = self.0.to_affine().coordinates().unwrap();
let other_coord = other.0.to_affine().coordinates().unwrap();
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): Even though self.0 cannot be the identity, I do not like that the .unwrap()s here. We should instead just compare the byte representations of self and other.

Comment thread src/note/asset_base.rs
impl AssetBase {
/// Deserialize the AssetBase from a byte array.
pub fn from_bytes(bytes: &[u8; 32]) -> CtOption<Self> {
pallas::Point::from_bytes(bytes).map(AssetBase)
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): Bug: this needs to reject the identity, in order to correctly enforce the requirements on AssetBase specified in ZIP 226 when parsing note plaintexts.

Comment thread src/note/asset_base.rs
///
/// Defined in [ZIP-227: Issuance of Zcash Shielded Assets][assetdigest].
///
/// [assetdigest]: https://zips.z.cash/zip-0227.html#specification-asset-identifier-asset-digest-and-asset-base
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): ```suggestion
/// Derives the Asset Digest for the given ZSA asset.
///
/// Defined in [ZIP-227: Issuance of Zcash Shielded Assets][assetdigest].
///
/// [assetdigest]: https://zips.z.cash/zip-0227#asset-digests

Comment thread src/note/asset_base.rs
version: u8,
ik: &IssueValidatingKey<ZSASchnorr>,
asset_desc_hash: &[u8; 32],
) -> Vec<u8> {
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): This function mis-represents the relationship between its arguments. version is not an arbitrary parameter independent from ik and asset_desc_hash: by definition, if version != 0, the ik and asset_desc_hash parameters might not exist.

Either remove the version parameter and internally hard-code it to 0 (making this function solely for the version 0 protocol), or replace all of the arguments with an AssetId enum:

pub enum AssetId {
    V0 {
        ik: IssueValidatingKey<ZSASchnorr>,
        asset_desc_hash: [u8; 32],
    },
}

and then make this function a method of AssetId (as well as asset_digest).

Comment thread src/note/asset_base.rs
let ik_encoding = ik.encode();
let mut asset_id = Vec::with_capacity(1 + ik_encoding.len() + asset_desc_hash.len());
asset_id.push(version);
asset_id.extend(ik_encoding);
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): While you're making changes here, update the variable names to match ZIP 227:

Suggested change
asset_id.extend(ik_encoding);
let issuer = ik.encode();
let mut asset_id = Vec::with_capacity(1 + issuer.len() + asset_desc_hash.len());
asset_id.push(version);
asset_id.extend(issuer);

Comment thread src/note/asset_base.rs
/// Defined in [ZIP-227: Issuance of Zcash Shielded Assets][assetdigest].
///
/// [assetdigest]: https://zips.z.cash/zip-0227.html#specification-asset-identifier-asset-digest-and-asset-base
pub fn asset_digest(encode_asset_id: &[u8]) -> Blake2bHash {
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): I don't like this being a public method and taking an arbitrary unconstrained slice. If you take my earlier suggestion of introducing an AssetId enum with this as one of its methods, then this would instead take &self which is fine.

Comment thread src/note/asset_base.rs

// EncodeAssetId(ik, asset_desc_hash) = version_byte || ik || asset_desc_hash
let asset_id = encode_asset_id(version_byte, ik, asset_desc_hash);
let asset_digest = asset_digest(&asset_id);
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): This should be named AssetBase::custom rather than AssetBase::derive, to match the "Custom Asset" terminology from ZIP 227.

If my enum suggestion is taken:

Suggested change
let asset_digest = asset_digest(&asset_id);
pub fn custom(asset_id: &AssetId) -> Self
let asset_digest = asset_id.asset_digest();

Comment thread src/note/asset_base.rs
///
/// Defined in [ZIP-226: Transfer and Burn of Zcash Shielded Assets][assetbase].
///
/// [assetbase]: https://zips.z.cash/zip-0226.html#asset-identifiers
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): ```suggestion
/// Defined in [ZIP 227: Issuance of Zcash Shielded Assets][assetbase].
///
/// [assetbase]: https://zips.z.cash/zip-0227#asset-bases

Comment thread src/note/asset_base.rs
}

/// Note type for the "native" currency (zec), maintains backward compatibility with Orchard untyped notes.
pub fn native() -> Self {
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): I just noticed that ZIP 227's terminology section defines "Native Asset" as "a Custom Asset with issuance defined on the Zcash blockchain" (first introduced in QED-it/zips@97eacab), where "Custom Asset" is "any Asset other than ZEC and TAZ" (first introduced in QED-it/zips@8ed51c2). Thus the entire usage of native / is_native in this PR is inconsistent with ZIP 227, and should be reworked to use a different term.

Non-blocking for this PR, but blocking the first orchard release containing this PR; open an issue for addressing this.

Comment thread src/note/asset_base.rs
pub fn native() -> Self {
AssetBase(pallas::Point::hash_to_curve(
VALUE_COMMITMENT_PERSONALIZATION,
)(&NATIVE_ASSET_BASE_V_BYTES[..]))
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): The slice notation should not be necessary hereh (it isn't present in the existing equivalent call that is deleted in this PR):

Suggested change
)(&NATIVE_ASSET_BASE_V_BYTES[..]))
)(&NATIVE_ASSET_BASE_V_BYTES))

Comment thread src/note/asset_base.rs
is_native in prop::bool::ANY,
isk in arb_issuance_authorizing_key(),
asset_desc_hash in any::<[u8; 32]>(),
) -> AssetBase {
Copy link
Copy Markdown
Author

@dmidem dmidem Dec 7, 2025

Choose a reason for hiding this comment

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

str4d (Link): Nit: we should only get arbitrary (isk, asset_desc_hash) if is_native; the generation should be conditional. I think you do this with a second sequence of arguments? We essentially want the equivalent of (!is_native).then(|| arb_zsa_asset_base()).

@dmidem
Copy link
Copy Markdown
Author

dmidem commented Dec 7, 2025

From upstream review (reply; parent not copied inline)

str4d (Link): In a pairing with daira, we also noted that currently ZIP 230 defines valueBurn: u64, but allowing that full range now is annoying because every other value balance is an i64, and it would force us to add a bunch of extra handling logic that would only be used for burns (and would be incompatible with future asset-specific value balances when we eventually have a subsequent pool that supports ZSAs via a turnstile).

For simplicity, we should update ZIP 230 to only allow burn amounts that fit into both a u64 and an i64, i.e. u63.

@dmidem
Copy link
Copy Markdown
Author

dmidem commented Dec 22, 2025

Closed in favour of #200

@dmidem dmidem closed this Dec 22, 2025
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.

7 participants