Issuance tests and Issue commitments. (#17)

Unit and property tests for issue bundle
Commitments for issuance
This commit is contained in:
Alexey Koren 2022-09-21 17:46:32 +02:00 committed by GitHub
parent 3902f755bd
commit 46085d7c1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 479 additions and 31 deletions

View File

@ -3,12 +3,15 @@
use blake2b_simd::{Hash as Blake2bHash, Params, State};
use crate::bundle::{Authorization, Authorized, Bundle};
use crate::issuance::{IssueAuth, IssueBundle, Signed};
const ZCASH_ORCHARD_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdOrchardHash";
const ZCASH_ORCHARD_ACTIONS_COMPACT_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdOrcActCHash";
const ZCASH_ORCHARD_ACTIONS_MEMOS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdOrcActMHash";
const ZCASH_ORCHARD_ACTIONS_NONCOMPACT_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxIdOrcActNHash";
const ZCASH_ORCHARD_SIGS_HASH_PERSONALIZATION: &[u8; 16] = b"ZTxAuthOrchaHash";
const ZCASH_ORCHARD_ZSA_ISSUE_PERSONALIZATION: &[u8; 16] = b"ZTxIdOrcZSAIssue";
const ZCASH_ORCHARD_ZSA_ISSUE_SIG_PERSONALIZATION: &[u8; 16] = b"ZTxAuthZSAOrHash";
fn hasher(personal: &[u8; 16]) -> State {
Params::new().hash_length(32).personal(personal).to_state()
@ -90,3 +93,30 @@ pub(crate) fn hash_bundle_auth_data<V>(bundle: &Bundle<Authorized, V>) -> Blake2
pub fn hash_bundle_auth_empty() -> Blake2bHash {
hasher(ZCASH_ORCHARD_SIGS_HASH_PERSONALIZATION).finalize()
}
/// Construct the commitment for the issue bundle
pub(crate) fn hash_issue_bundle_txid_data<A: IssueAuth>(bundle: &IssueBundle<A>) -> Blake2bHash {
let mut h = hasher(ZCASH_ORCHARD_ZSA_ISSUE_PERSONALIZATION);
for action in bundle.actions().iter() {
for note in action.notes().iter() {
h.update(&note.recipient().to_raw_address_bytes());
h.update(&note.value().to_bytes());
h.update(&note.note_type().to_bytes());
h.update(&note.rho().to_bytes());
h.update(note.rseed().as_bytes());
}
h.update(action.asset_desc().as_bytes());
h.update(&[u8::from(action.is_finalized())]);
}
h.update(&bundle.ik().to_bytes());
h.finalize()
}
/// Construct the commitment to the authorizing data of an
/// authorized issue bundle
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.finalize()
}

View File

@ -1,10 +1,16 @@
//! Structs related to issuance bundles and the associated logic.
use blake2b_simd::Hash as Blake2bHash;
use nonempty::NonEmpty;
use rand::{CryptoRng, RngCore};
use std::collections::HashSet;
use std::fmt;
use crate::issuance::Error::{IssueActionPreviouslyFinalizedNoteType, IssueBundleInvalidSignature};
use crate::bundle::commitments::{hash_issue_bundle_auth_data, hash_issue_bundle_txid_data};
use crate::issuance::Error::{
IssueActionAlreadyFinalized, IssueActionIncorrectNoteType, IssueActionNotFound,
IssueActionPreviouslyFinalizedNoteType, IssueBundleIkMismatchNoteType,
IssueBundleInvalidSignature, WrongAssetDescSize,
};
use crate::keys::{IssuerAuthorizingKey, IssuerValidatingKey};
use crate::note::note_type::MAX_ASSET_DESCRIPTION_SIZE;
use crate::note::{NoteType, Nullifier};
@ -88,12 +94,12 @@ impl IssueAction {
note.note_type()
.eq(&note_type)
.then(|| note_type)
.ok_or(Error::IssueActionIncorrectNoteType)
.ok_or(IssueActionIncorrectNoteType)
}) {
Ok(note_type) => note_type // check that the note_type was properly derived.
.eq(&NoteType::derive(ik, &self.asset_desc))
.then(|| note_type)
.ok_or(Error::IssueBundleIkMismatchNoteType),
.ok_or(IssueBundleIkMismatchNoteType),
Err(e) => Err(e),
}
}
@ -118,6 +124,13 @@ pub struct Signed {
signature: redpallas::Signature<SpendAuth>,
}
impl Signed {
/// Returns the signature for this authorization.
pub fn signature(&self) -> &redpallas::Signature<SpendAuth> {
&self.signature
}
}
impl IssueAuth for Unauthorized {}
impl IssueAuth for Prepared {}
impl IssueAuth for Signed {}
@ -150,6 +163,12 @@ impl<T: IssueAuth> IssueBundle<T> {
.find(|a| NoteType::derive(&self.ik, &a.asset_desc).eq(&note_type));
action
}
/// Computes a commitment to the effects of this bundle, suitable for inclusion within
/// a transaction ID.
pub fn commitment(&self) -> IssueBundleCommitment {
IssueBundleCommitment(hash_issue_bundle_txid_data(self))
}
}
impl IssueBundle<Unauthorized> {
@ -178,7 +197,7 @@ impl IssueBundle<Unauthorized> {
mut rng: impl RngCore,
) -> Result<NoteType, Error> {
if !is_asset_desc_valid(&asset_desc) {
return Err(Error::WrongAssetDescSize);
return Err(WrongAssetDescSize);
}
let note_type = NoteType::derive(&self.ik, &asset_desc);
@ -199,7 +218,7 @@ impl IssueBundle<Unauthorized> {
// Append to an existing IssueAction.
Some(action) => {
if action.finalize {
return Err(Error::IssueActionAlreadyFinalized);
return Err(IssueActionAlreadyFinalized);
};
action.notes.push(note);
finalize.then(|| action.finalize = true);
@ -222,7 +241,7 @@ impl IssueBundle<Unauthorized> {
/// Panics if `asset_desc` is empty or longer than 512 bytes.
pub fn finalize_action(&mut self, asset_desc: String) -> Result<(), Error> {
if !is_asset_desc_valid(&asset_desc) {
return Err(Error::WrongAssetDescSize);
return Err(WrongAssetDescSize);
}
match self
@ -234,7 +253,7 @@ impl IssueBundle<Unauthorized> {
issue_action.finalize = true;
}
None => {
return Err(Error::IssueActionNotFound);
return Err(IssueActionNotFound);
}
}
@ -280,6 +299,34 @@ impl IssueBundle<Prepared> {
}
}
/// A commitment to a bundle of actions.
///
/// This commitment is non-malleable, in the sense that a bundle's commitment will only
/// change if the effects of the bundle are altered.
#[derive(Debug)]
pub struct IssueBundleCommitment(pub Blake2bHash);
impl From<IssueBundleCommitment> for [u8; 32] {
/// Serializes issue bundle commitment as byte array
fn from(commitment: IssueBundleCommitment) -> Self {
// The commitment uses BLAKE2b-256.
commitment.0.as_bytes().try_into().unwrap()
}
}
/// A commitment to the authorizing data within a bundle of actions.
#[derive(Debug)]
pub struct IssueBundleAuthorizingCommitment(pub Blake2bHash);
impl IssueBundle<Signed> {
/// Computes a commitment to the authorizing data within for this bundle.
///
/// This together with `IssueBundle::commitment` bind the entire bundle.
pub fn authorizing_commitment(&self) -> IssueBundleAuthorizingCommitment {
IssueBundleAuthorizingCommitment(hash_issue_bundle_auth_data(self))
}
}
fn is_asset_desc_valid(asset_desc: &str) -> bool {
!asset_desc.is_empty() && asset_desc.bytes().len() <= MAX_ASSET_DESCRIPTION_SIZE
}
@ -312,7 +359,7 @@ pub fn verify_issue_bundle<'a>(
.iter()
.try_fold(previously_finalized, |acc, action| {
if !is_asset_desc_valid(action.asset_desc()) {
return Err(Error::WrongAssetDescSize);
return Err(WrongAssetDescSize);
}
// Fail if any note in the IssueAction has incorrect note type.
@ -364,38 +411,62 @@ pub enum Error {
IssueActionPreviouslyFinalizedNoteType(NoteType),
}
// impl std::error::Error for Error {}
//
// impl fmt::Display for Error {
// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// match self {
// Error::IssueActionAlreadyFinalized => {write!(f, "unable to add note to the IssueAction since it has already been finalized")}
// Error::IssueActionNotFound => {}
// Error::IssueActionIncorrectNoteType => {}
// Error::IssueBundleIkMismatchNoteType => {}
// Error::WrongAssetDescSize => {}
// IssueBundleInvalidSignature(_) => {}
// IssueActionPreviouslyFinalizedNoteType(_) => {}
// }
// }
// }
impl std::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
IssueActionAlreadyFinalized => {
write!(
f,
"unable to add note to the IssueAction since it has already been finalized"
)
}
IssueActionNotFound => {
write!(f, "the requested IssueAction not exists in the bundle.")
}
IssueActionIncorrectNoteType => {
write!(f, "not all `NoteType`s are the same inside the action")
}
IssueBundleIkMismatchNoteType => {
write!(
f,
"the provided `isk` and the driven `ik` does not match at least one note type"
)
}
WrongAssetDescSize => {
write!(f, "`asset_desc` should be between 1 and 512 bytes")
}
IssueBundleInvalidSignature(_) => {
write!(f, "invalid signature")
}
IssueActionPreviouslyFinalizedNoteType(_) => {
write!(f, "the provided `NoteType` has been previously finalized")
}
}
}
}
#[cfg(test)]
mod tests {
use super::IssueBundle;
use crate::issuance::Error::{
IssueActionAlreadyFinalized, IssueActionNotFound, IssueActionPreviouslyFinalizedNoteType,
IssueBundleIkMismatchNoteType, WrongAssetDescSize,
IssueActionAlreadyFinalized, IssueActionIncorrectNoteType, IssueActionNotFound,
IssueActionPreviouslyFinalizedNoteType, IssueBundleIkMismatchNoteType,
IssueBundleInvalidSignature, WrongAssetDescSize,
};
use crate::issuance::{verify_issue_bundle, Error, Signed};
use crate::issuance::{verify_issue_bundle, IssueAction, Signed};
use crate::keys::{
FullViewingKey, IssuerAuthorizingKey, IssuerValidatingKey, Scope, SpendingKey,
};
use crate::note::NoteType;
use crate::note::{NoteType, Nullifier};
use crate::value::NoteValue;
use crate::Address;
use crate::{Address, Note};
use nonempty::NonEmpty;
use rand::rngs::OsRng;
use rand::RngCore;
use reddsa::Error::InvalidSignature;
use std::borrow::BorrowMut;
use std::collections::HashSet;
fn setup_params() -> (
@ -441,6 +512,19 @@ mod tests {
WrongAssetDescSize
);
assert_eq!(
bundle
.add_recipient(
"".to_string(),
recipient,
NoteValue::unsplittable(),
true,
rng,
)
.unwrap_err(),
WrongAssetDescSize
);
let note_type = bundle
.add_recipient(str.clone(), recipient, NoteValue::from_raw(5), false, rng)
.unwrap();
@ -521,6 +605,11 @@ mod tests {
WrongAssetDescSize
);
assert_eq!(
bundle.finalize_action("".to_string()).unwrap_err(),
WrongAssetDescSize
);
bundle
.add_recipient(
String::from("Another precious NFT"),
@ -613,6 +702,47 @@ mod tests {
assert_eq!(err, IssueBundleIkMismatchNoteType);
}
#[test]
fn issue_bundle_incorrect_note_type_for_signature() {
let (mut rng, isk, ik, recipient, _) = setup_params();
let mut bundle = IssueBundle::new(ik);
// Add "normal" note
bundle
.add_recipient(
String::from("IssueBundle"),
recipient,
NoteValue::from_raw(5),
false,
rng,
)
.unwrap();
// Add "bad" note
let note = Note::new(
recipient,
NoteValue::from_raw(5),
NoteType::derive(bundle.ik(), "Poisoned pill"),
Nullifier::dummy(&mut rng),
&mut rng,
);
bundle
.actions
.first_mut()
.unwrap()
.notes
.borrow_mut()
.push(note);
let err = bundle
.prepare([0; 32])
.sign(rng, &isk)
.expect_err("should not be able to sign");
assert_eq!(err, IssueActionIncorrectNoteType);
}
#[test]
fn issue_bundle_verify() {
let (rng, isk, ik, recipient, sighash) = setup_params();
@ -730,7 +860,245 @@ mod tests {
assert_eq!(
verify_issue_bundle(&signed, sighash, prev_finalized).unwrap_err(),
Error::IssueBundleInvalidSignature(reddsa::Error::InvalidSignature)
IssueBundleInvalidSignature(InvalidSignature)
);
}
#[test]
fn issue_bundle_verify_fail_wrong_sighash() {
let (rng, isk, ik, recipient, random_sighash) = setup_params();
let mut bundle = IssueBundle::new(ik);
bundle
.add_recipient(
String::from("Good description"),
recipient,
NoteValue::from_raw(5),
false,
rng,
)
.unwrap();
let sighash: [u8; 32] = bundle.commitment().into();
let signed = bundle.prepare(sighash).sign(rng, &isk).unwrap();
let prev_finalized = &mut HashSet::new();
// 2. Try empty description
let finalized = verify_issue_bundle(&signed, random_sighash, prev_finalized);
assert_eq!(
finalized.unwrap_err(),
IssueBundleInvalidSignature(InvalidSignature)
);
}
#[test]
fn issue_bundle_verify_fail_incorrect_asset_description() {
let (mut rng, isk, ik, recipient, sighash) = setup_params();
let mut bundle = IssueBundle::new(ik);
bundle
.add_recipient(
String::from("Good description"),
recipient,
NoteValue::from_raw(5),
false,
rng,
)
.unwrap();
let mut signed = bundle.prepare(sighash).sign(rng, &isk).unwrap();
// Add "bad" note
let note = Note::new(
recipient,
NoteValue::from_raw(5),
NoteType::derive(signed.ik(), "Poisoned pill"),
Nullifier::dummy(&mut rng),
&mut rng,
);
signed
.actions
.first_mut()
.unwrap()
.notes
.borrow_mut()
.push(note);
let prev_finalized = &mut HashSet::new();
let err = verify_issue_bundle(&signed, sighash, prev_finalized).unwrap_err();
assert_eq!(err, IssueActionIncorrectNoteType);
}
#[test]
fn issue_bundle_verify_fail_incorrect_ik() {
let asset_description = "asset";
let (mut rng, isk, ik, recipient, sighash) = setup_params();
let mut bundle = IssueBundle::new(ik);
bundle
.add_recipient(
String::from(asset_description),
recipient,
NoteValue::from_raw(5),
false,
rng,
)
.unwrap();
let mut signed = bundle.prepare(sighash).sign(rng, &isk).unwrap();
let incorrect_sk = SpendingKey::random(&mut rng);
let incorrect_isk: IssuerAuthorizingKey = (&incorrect_sk).into();
let incorrect_ik: IssuerValidatingKey = (&incorrect_isk).into();
// Add "bad" note
let note = Note::new(
recipient,
NoteValue::from_raw(55),
NoteType::derive(&incorrect_ik, asset_description),
Nullifier::dummy(&mut rng),
&mut rng,
);
signed.actions.first_mut().unwrap().notes = NonEmpty::new(note);
let prev_finalized = &mut HashSet::new();
let err = verify_issue_bundle(&signed, sighash, prev_finalized).unwrap_err();
assert_eq!(err, IssueBundleIkMismatchNoteType);
}
#[test]
fn issue_bundle_verify_fail_wrong_asset_descr_size() {
// we want to inject "bad" description for test purposes.
impl IssueAction {
pub fn modify_descr(&mut self, new_descr: String) {
self.asset_desc = new_descr;
}
}
let (rng, isk, ik, recipient, sighash) = setup_params();
let mut bundle = IssueBundle::new(ik);
bundle
.add_recipient(
String::from("Good description"),
recipient,
NoteValue::from_raw(5),
false,
rng,
)
.unwrap();
let mut signed = bundle.prepare(sighash).sign(rng, &isk).unwrap();
let prev_finalized = &mut HashSet::new();
// 1. Try too long description
signed
.actions
.first_mut()
.unwrap()
.modify_descr(String::from_utf8(vec![b'X'; 513]).unwrap());
let finalized = verify_issue_bundle(&signed, sighash, prev_finalized);
assert_eq!(finalized.unwrap_err(), WrongAssetDescSize);
// 2. Try empty description
signed
.actions
.first_mut()
.unwrap()
.modify_descr("".to_string());
let finalized = verify_issue_bundle(&signed, sighash, prev_finalized);
assert_eq!(finalized.unwrap_err(), WrongAssetDescSize);
}
}
/// Generators for property testing.
#[cfg(any(test, feature = "test-dependencies"))]
#[cfg_attr(docsrs, doc(cfg(feature = "test-dependencies")))]
pub mod testing {
use crate::issuance::{IssueAction, IssueBundle, Prepared, Signed, Unauthorized};
use crate::keys::testing::{arb_issuer_authorizing_key, arb_issuer_validating_key};
use crate::note::testing::arb_zsa_note;
use proptest::collection::vec;
use proptest::prelude::*;
use proptest::prop_compose;
use proptest::string::string_regex;
use rand::{rngs::StdRng, SeedableRng};
prop_compose! {
/// Generate an issue action given note value
pub fn arb_issue_action()(
note in arb_zsa_note(),
asset_descr in string_regex(".{1,512}").unwrap()
) -> IssueAction {
IssueAction::new(asset_descr, &note)
}
}
prop_compose! {
/// Generate an arbitrary issue bundle with fake authorization data. This bundle does not
/// necessarily respect consensus rules; for that use
/// [`crate::builder::testing::arb_issue_bundle`]
pub fn arb_unathorized_issue_bundle(n_actions: usize)
(
actions in vec(arb_issue_action(), n_actions),
ik in arb_issuer_validating_key()
) -> IssueBundle<Unauthorized> {
IssueBundle {
ik,
actions,
authorization: Unauthorized
}
}
}
prop_compose! {
/// Generate an arbitrary issue bundle with fake authorization data. This bundle does not
/// necessarily respect consensus rules; for that use
/// [`crate::builder::testing::arb_issue_bundle`]
pub fn arb_prepared_issue_bundle(n_actions: usize)
(
actions in vec(arb_issue_action(), n_actions),
ik in arb_issuer_validating_key(),
fake_sighash in prop::array::uniform32(prop::num::u8::ANY)
) -> IssueBundle<Prepared> {
IssueBundle {
ik,
actions,
authorization: Prepared { sighash: fake_sighash }
}
}
}
prop_compose! {
/// Generate an arbitrary issue bundle with fake authorization data. This bundle does not
/// necessarily respect consensus rules; for that use
/// [`crate::builder::testing::arb_issue_bundle`]
pub fn arb_signed_issue_bundle(n_actions: usize)
(
actions in vec(arb_issue_action(), n_actions),
ik in arb_issuer_validating_key(),
isk in arb_issuer_authorizing_key(),
rng_seed in prop::array::uniform32(prop::num::u8::ANY),
fake_sighash in prop::array::uniform32(prop::num::u8::ANY)
) -> IssueBundle<Signed> {
let rng = StdRng::from_seed(rng_seed);
IssueBundle {
ik,
actions,
authorization: Prepared { sighash: fake_sighash },
}.sign(rng, &isk).unwrap()
}
}
}

View File

@ -1003,9 +1003,12 @@ impl SharedSecret {
#[cfg(any(test, feature = "test-dependencies"))]
#[cfg_attr(docsrs, doc(cfg(feature = "test-dependencies")))]
pub mod testing {
use super::{
DiversifierIndex, DiversifierKey, EphemeralSecretKey, IssuerAuthorizingKey,
IssuerValidatingKey, SpendingKey,
};
use proptest::prelude::*;
use super::{DiversifierIndex, DiversifierKey, EphemeralSecretKey, SpendingKey};
use rand::{rngs::StdRng, SeedableRng};
prop_compose! {
/// Generate a uniformly distributed Orchard spending key.
@ -1052,6 +1055,21 @@ pub mod testing {
DiversifierIndex::from(d_bytes)
}
}
prop_compose! {
/// Generate a uniformly distributed RedDSA issuer authorizing key.
pub fn arb_issuer_authorizing_key()(rng_seed in prop::array::uniform32(prop::num::u8::ANY)) -> IssuerAuthorizingKey {
let mut rng = StdRng::from_seed(rng_seed);
IssuerAuthorizingKey::from(&SpendingKey::random(&mut rng))
}
}
prop_compose! {
/// Generate a uniformly distributed RedDSA issuer validating key.
pub fn arb_issuer_validating_key()(isk in arb_issuer_authorizing_key()) -> IssuerValidatingKey {
IssuerValidatingKey::from(&isk)
}
}
}
#[cfg(test)]

View File

@ -284,6 +284,8 @@ pub mod testing {
use proptest::prelude::*;
use crate::note::note_type::testing::arb_note_type;
use crate::note::note_type::testing::zsa_note_type;
use crate::value::testing::arb_note_value;
use crate::{
address::testing::arb_address, note::nullifier::testing::arb_nullifier, value::NoteValue,
};
@ -314,4 +316,23 @@ pub mod testing {
}
}
}
prop_compose! {
/// Generate an arbitrary ZSA note
pub fn arb_zsa_note()(
recipient in arb_address(),
value in arb_note_value(),
rho in arb_nullifier(),
rseed in arb_rseed(),
note_type in zsa_note_type(),
) -> Note {
Note {
recipient,
value,
note_type,
rho,
rseed,
}
}
}
}

View File

@ -110,4 +110,15 @@ pub mod testing {
NoteType::native()
}
}
prop_compose! {
/// Generate the ZSA note type
pub fn zsa_note_type()(
sk in arb_spending_key(),
str in "[A-Za-z]{255}"
) -> NoteType {
let isk = IssuerAuthorizingKey::from(&sk);
NoteType::derive(&IssuerValidatingKey::from(&isk), &str)
}
}
}