Skip to content
2 changes: 1 addition & 1 deletion src/bundle/commitments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ pub(crate) fn hash_issue_bundle_txid_data<A: IssueAuth>(bundle: &IssueBundle<A>)
/// [zip246]: https://zips.z.cash/zip-0246
pub(crate) fn hash_issue_bundle_auth_data(bundle: &IssueBundle<Signed>) -> Blake2bHash {
let mut h = hasher(ZCASH_ORCHARD_ZSA_ISSUE_SIG_PERSONALIZATION);
h.update(&<[u8; 64]>::from(bundle.authorization().signature()));
h.update(&bundle.authorization().signature().to_bytes());
h.finalize()
}

Expand Down
73 changes: 63 additions & 10 deletions src/issuance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::{
asset_record::AssetRecord,
bundle::commitments::{hash_issue_bundle_auth_data, hash_issue_bundle_txid_data},
constants::reference_keys::ReferenceKeys,
keys::{IssuanceAuthorizingKey, IssuanceValidatingKey},
keys::{IssuanceAuthSigScheme, IssuanceAuthorizingKey, IssuanceValidatingKey},
note::{AssetBase, Nullifier, Rho},
value::NoteValue,
Address, Note,
Expand All @@ -33,7 +33,7 @@ use crate::{
use Error::{
AssetBaseCannotBeIdentityPoint, CannotBeFirstIssuance, IssueActionNotFound,
IssueActionPreviouslyFinalizedAssetBase, IssueActionWithoutNoteNotFinalized,
IssueBundleIkMismatchAssetBase, IssueBundleInvalidSignature,
IssueAuthSigGenerationFailed, IssueBundleIkMismatchAssetBase, IssueBundleInvalidSignature,
MissingReferenceNoteOnFirstIssuance, ValueOverflow,
};

Expand Down Expand Up @@ -79,6 +79,47 @@ pub struct IssueInfo {
pub value: NoteValue,
}

/// The type of an Issuance Authorization Signature
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IssuanceAuthorizationSignature {
scheme: IssuanceAuthSigScheme,
signature: schnorr::Signature,
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.

I think signature should be a [u8] or Vec because we would like to have a generic implementation which might not be a Schnorr signature

}

impl IssuanceAuthorizationSignature {
/// Constructs a new `IssuanceAuthorizationSignature`.
pub fn new(scheme: IssuanceAuthSigScheme, signature: schnorr::Signature) -> Self {
IssuanceAuthorizationSignature { scheme, signature }
}

/// Returns the scheme of the signature.
pub fn scheme(&self) -> &IssuanceAuthSigScheme {
&self.scheme
}

/// Returns the signature.
pub fn signature(&self) -> &schnorr::Signature {
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.

Idem
This function should return &[u8] or &Vec

&self.signature
}

/// Returns the byte encoding of the signature.
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = vec![self.scheme as u8];
bytes.extend(self.signature.to_bytes());
Comment on lines +106 to +108
Copy link

Copilot AI Jul 17, 2025

Choose a reason for hiding this comment

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

Similar to IssuanceValidatingKey, returning Vec instead of a fixed-size array reduces type safety. Consider returning [u8; 65] for consistency with the scheme's defined signature length.

Suggested change
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = vec![self.scheme as u8];
bytes.extend(self.signature.to_bytes());
pub fn to_bytes(&self) -> [u8; 65] {
let mut bytes = [0u8; 65];
bytes[0] = self.scheme as u8;
bytes[1..].copy_from_slice(&self.signature.to_bytes());

Copilot uses AI. Check for mistakes.
assert_eq!(bytes.len(), self.scheme.details().sig_length);
bytes
}

/// Constructs an `IssuanceAuthorizationSignature` from a byte array.
pub fn from_bytes(bytes: &[u8; 65]) -> Result<Self, Error> {
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.

Replace [u8; 65] with [u8] or Vec because the length of the sig depends on the scheme

let scheme = IssuanceAuthSigScheme::from_key_algorithm_byte(bytes[0])
.ok_or(IssueBundleInvalidSignature)?;
let signature =
schnorr::Signature::try_from(&bytes[1..]).map_err(|_| IssueBundleInvalidSignature)?;
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.

The signature might not be the Schnorr signature

Ok(IssuanceAuthorizationSignature { scheme, signature })
}
}

/// Compute the asset description hash for a given asset description.
///
/// # Panics
Expand Down Expand Up @@ -240,19 +281,19 @@ pub struct Prepared {
/// Marker for an authorized bundle.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Signed {
signature: schnorr::Signature,
signature: IssuanceAuthorizationSignature,
}

impl Signed {
/// Returns the signature for this authorization.
pub fn signature(&self) -> &schnorr::Signature {
pub fn signature(&self) -> &IssuanceAuthorizationSignature {
&self.signature
}

/// Constructs a `Signed` from a byte array containing Schnorr signature bytes.
pub fn from_data(data: [u8; 64]) -> Self {
pub fn from_data(data: [u8; 65]) -> Self {
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.

Replace [u8; 65] with [u8] or Vec because the length of the sig depends on the scheme

Signed {
signature: schnorr::Signature::try_from(data.as_ref()).unwrap(),
signature: IssuanceAuthorizationSignature::from_bytes(&data).unwrap(),
}
}
}
Expand Down Expand Up @@ -687,6 +728,9 @@ pub enum Error {
/// It cannot be first issuance because we have already some notes for this asset.
CannotBeFirstIssuance,

/// The generation of the Issuance Authorization Signature failed.
IssueAuthSigGenerationFailed, //TODO: VA: This does not propagate the schnorr::Error, fix it.
Copy link

Copilot AI Jul 17, 2025

Choose a reason for hiding this comment

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

The TODO comment indicates incomplete error handling. Consider either implementing proper error propagation from schnorr::Error or removing the TODO if this is intentional for now.

Copilot uses AI. Check for mistakes.

/// Verification errors:
/// Invalid signature.
IssueBundleInvalidSignature,
Expand Down Expand Up @@ -730,6 +774,9 @@ impl fmt::Display for Error {
"it cannot be first issuance because we have already some notes for this asset."
)
}
IssueAuthSigGenerationFailed => {
write!(f, "failed to generate the Issuance Authorization Signature")
}
IssueBundleInvalidSignature => {
write!(f, "invalid signature")
}
Expand Down Expand Up @@ -1848,8 +1895,11 @@ mod tests {
#[cfg_attr(docsrs, doc(cfg(feature = "test-dependencies")))]
pub mod testing {
use crate::{
issuance::{AwaitingNullifier, IssueAction, IssueBundle, Prepared, Signed},
keys::testing::arb_issuance_validating_key,
issuance::{
AwaitingNullifier, IssuanceAuthorizationSignature, IssueAction, IssueBundle, Prepared,
Signed,
},
keys::{testing::arb_issuance_validating_key, IssuanceAuthSigScheme},
note::asset_base::testing::zsa_asset_base,
note::testing::arb_zsa_note,
};
Expand All @@ -1863,8 +1913,11 @@ pub mod testing {
/// Generate a uniformly distributed signature
pub(crate) fn arb_signature()(
sig_bytes in vec(prop::num::u8::ANY, 64)
) -> schnorr::Signature {
schnorr::Signature::try_from(sig_bytes.as_slice()).unwrap()
) -> IssuanceAuthorizationSignature {
IssuanceAuthorizationSignature::new(
IssuanceAuthSigScheme::ZsaSchnorrSigV1,
schnorr::Signature::try_from(sig_bytes.as_slice()).unwrap()
)
}
}

Expand Down
124 changes: 105 additions & 19 deletions src/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ use group::{
};
use k256::{
schnorr,
schnorr::{
signature::hazmat::{PrehashSigner, PrehashVerifier},
Signature, VerifyingKey,
},
schnorr::signature::hazmat::{PrehashSigner, PrehashVerifier},
NonZeroScalar,
};
use pasta_curves::{pallas, pallas::Scalar};
Expand All @@ -28,6 +25,8 @@ use zcash_note_encryption::EphemeralKeyBytes;

use crate::{
address::Address,
issuance,
issuance::IssuanceAuthorizationSignature,
primitives::redpallas::{self, SpendAuth, VerificationKey},
spec::{
commit_ivk, diversify_hash, extract_p, ka_orchard, ka_orchard_prepared, prf_nf, to_base,
Expand Down Expand Up @@ -239,6 +238,68 @@ fn check_structural_validity(
}
}

/// Complete, immutable description of a supported scheme.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct IssuanceAuthSigDetails {
/// The byte that identifies the key algorithm.
pub key_algorithm_byte: u8,
/// The length of the issuance validating key for the scheme.
pub key_length: usize,
/// The length of the issuance authorization signature for the scheme.
pub sig_length: usize,
}

/// Enumeration of schemes.
///
/// `#[repr(u8)]` makes the discriminant *equal* to `key_algorithm_byte`,
/// so the mapping is just a cast.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum IssuanceAuthSigScheme {
/// OrchardZSA Schnorr/BIP-340 (ZIP-227), version 1.
ZsaSchnorrSigV1 = 0x00,
}

impl IssuanceAuthSigScheme {
/* ───── associated constants ───── */

/// These are the constants of the [ZIP 227][issuanceauthsig] Schnorr signature scheme based on BIP 340.
///
/// [issuanceauthsig]: https://zips.z.cash/zip-0227#orchard-zsa-issuance-authorization-signature-scheme
pub const ZSA_SCHNORR_SIG_V1_DETAILS: IssuanceAuthSigDetails = IssuanceAuthSigDetails {
key_algorithm_byte: Self::ZsaSchnorrSigV1 as u8,
key_length: 33,
sig_length: 65,
};

/// Returns the details of the specific issuance authorization signature scheme.
pub const fn details(self) -> &'static IssuanceAuthSigDetails {
match self {
Self::ZsaSchnorrSigV1 => &Self::ZSA_SCHNORR_SIG_V1_DETAILS,
}
}

/// Returns the signature scheme being used based on the value of the key algorithm byte.
pub const fn from_key_algorithm_byte(b: u8) -> Option<Self> {
match b {
x if x == Self::ZsaSchnorrSigV1 as u8 => Some(Self::ZsaSchnorrSigV1),
_ => None,
}
}
}
impl TryFrom<u8> for IssuanceAuthSigScheme {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
IssuanceAuthSigScheme::from_key_algorithm_byte(value).ok_or(())
}
}

impl From<IssuanceAuthSigScheme> for u8 {
fn from(s: IssuanceAuthSigScheme) -> Self {
s.details().key_algorithm_byte
}
}

/// An issuance key, from which all key material is derived.
///
/// $\mathsf{isk}$ as defined in [ZIP 227][issuancekeycomponents].
Expand Down Expand Up @@ -295,8 +356,17 @@ impl IssuanceAuthorizingKey {

/// Sign the provided message using the `IssuanceAuthorizingKey`.
/// Only supports signing of messages of length 32 bytes, since we will only be using it to sign 32 byte SIGHASH values.
pub fn try_sign(&self, msg: &[u8; 32]) -> Result<Signature, schnorr::Error> {
schnorr::SigningKey::from(self.0).sign_prehash(msg)
pub fn try_sign(
&self,
msg: &[u8; 32],
) -> Result<IssuanceAuthorizationSignature, issuance::Error> {
let signature = schnorr::SigningKey::from(self.0)
.sign_prehash(msg)
.map_err(|_| issuance::Error::IssueAuthSigGenerationFailed)?;
Ok(IssuanceAuthorizationSignature::new(
IssuanceAuthSigScheme::ZsaSchnorrSigV1,
signature,
))
}
}

Expand All @@ -314,11 +384,17 @@ impl Debug for IssuanceAuthorizingKey {
///
/// [IssuanceZSA]: https://zips.z.cash/zip-0227#issuance-key-derivation
#[derive(Debug, Clone)]
pub struct IssuanceValidatingKey(schnorr::VerifyingKey);
pub struct IssuanceValidatingKey {
scheme: IssuanceAuthSigScheme,
key: schnorr::VerifyingKey,
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.

Replace schnorr::VerifyingKey by [u8] or Vec

}

impl From<&IssuanceAuthorizingKey> for IssuanceValidatingKey {
fn from(isk: &IssuanceAuthorizingKey) -> Self {
IssuanceValidatingKey(*schnorr::SigningKey::from(isk.0).verifying_key())
IssuanceValidatingKey {
scheme: IssuanceAuthSigScheme::ZsaSchnorrSigV1,
key: *schnorr::SigningKey::from(isk.0).verifying_key(),
}
}
}

Expand All @@ -331,25 +407,36 @@ impl PartialEq for IssuanceValidatingKey {
impl Eq for IssuanceValidatingKey {}

impl IssuanceValidatingKey {
/// Converts this issuance validating key to its serialized form,
/// in big-endian order as defined in BIP 340.
pub fn to_bytes(&self) -> [u8; 32] {
self.0.to_bytes().into()
/// Converts this issuance validating key to its serialized form, with a scheme byte prefix,
/// and the key in big-endian order as defined in BIP 340.
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = vec![self.scheme as u8];
bytes.extend(self.key.to_bytes());
Comment on lines +412 to +414
Copy link

Copilot AI Jul 17, 2025

Choose a reason for hiding this comment

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

Returning Vec for key serialization is inconsistent with other key types that return fixed-size arrays. Consider returning a fixed-size array like [u8; 33] for better type safety and consistency.

Suggested change
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = vec![self.scheme as u8];
bytes.extend(self.key.to_bytes());
pub fn to_bytes(&self) -> [u8; 33] {
let mut bytes = [0u8; 33];
bytes[0] = self.scheme as u8;
bytes[1..].copy_from_slice(&self.key.to_bytes());

Copilot uses AI. Check for mistakes.
assert_eq!(bytes.len(), self.scheme.details().key_length);
bytes
}

/// Constructs an Orchard issuance validating key from the provided bytes.
/// The bytes are assumed to be encoded in big-endian order.
///
/// Returns `None` if the bytes do not correspond to a valid key.
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
VerifyingKey::from_bytes(bytes)
.ok()
.map(IssuanceValidatingKey)
IssuanceAuthSigScheme::from_key_algorithm_byte(bytes[0]).and_then(|scheme| {
schnorr::VerifyingKey::from_bytes(&bytes[1..])
.ok()
.map(|key| IssuanceValidatingKey { scheme, key })
})
}

/// Verifies a purported `signature` over `msg` made by this verification key.
pub fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), schnorr::Error> {
self.0.verify_prehash(msg, signature)
pub fn verify(
&self,
msg: &[u8],
sig: &IssuanceAuthorizationSignature,
) -> Result<(), issuance::Error> {
self.key
.verify_prehash(msg, sig.signature())
.map_err(|_| issuance::Error::IssueBundleInvalidSignature)
}
}

Expand Down Expand Up @@ -1272,8 +1359,7 @@ mod tests {
let message = tv.msg;

let signature = isk.try_sign(&message).unwrap();
let sig_bytes: [u8; 64] = signature.to_bytes();
assert_eq!(sig_bytes, tv.sig);
assert_eq!(signature.to_bytes().as_slice(), &tv.sig);

assert!(ik.verify(&message, &signature).is_ok());
}
Expand Down
10 changes: 5 additions & 5 deletions src/note/asset_base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub const ZSA_ASSET_DIGEST_PERSONALIZATION: &[u8; 16] = b"ZSA-Asset-Digest";
/// 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; 65]) -> Blake2bHash {
pub fn asset_digest(encode_asset_id: [u8; 66]) -> Blake2bHash {
Params::new()
.hash_length(64)
.personal(ZSA_ASSET_DIGEST_PERSONALIZATION)
Expand Down Expand Up @@ -80,11 +80,11 @@ impl AssetBase {
let version_byte = [0x00];

// EncodeAssetId(ik, asset_desc_hash) = version_byte || ik || asset_desc_hash
let encode_asset_id: [u8; 65] = {
let mut array = [0u8; 65];
let encode_asset_id: [u8; 66] = {
let mut array = [0u8; 66];
array[..1].copy_from_slice(&version_byte);
array[1..33].copy_from_slice(&ik.to_bytes());
array[33..].copy_from_slice(asset_desc_hash);
array[1..34].copy_from_slice(&ik.to_bytes());
array[34..].copy_from_slice(asset_desc_hash);
array
};

Expand Down
Loading
Loading