E2E tests for issuance (#20)

added tests in `tests/zsa.rs`
This commit is contained in:
Alexey Koren 2022-10-20 15:43:18 +02:00 committed by Paul
parent 0b2988acc7
commit 9b434976ab
5 changed files with 488 additions and 31 deletions

View File

@ -75,7 +75,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: tarpaulin
args: --all-features --timeout 600 --out Xml
args: --all-features --timeout 1200 --out Xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
with:

View File

@ -133,7 +133,7 @@ impl RecipientInfo {
/// Defined in [Zcash Protocol Spec § 4.8.3: Dummy Notes (Orchard)][orcharddummynotes].
///
/// [orcharddummynotes]: https://zips.z.cash/protocol/nu5.pdf#orcharddummynotes
fn dummy(rng: &mut impl RngCore) -> Self {
fn dummy(rng: &mut impl RngCore, note_type: NoteType) -> Self {
let fvk: FullViewingKey = (&SpendingKey::random(rng)).into();
let recipient = fvk.address_at(0u32, Scope::External);
@ -141,7 +141,7 @@ impl RecipientInfo {
ovk: None,
recipient,
value: NoteValue::zero(),
note_type: NoteType::native(),
note_type,
memo: None,
}
}
@ -167,16 +167,13 @@ impl ActionInfo {
/// Returns the value sum for this action.
/// Split notes does not contribute to the value sum.
fn value_sum(&self) -> ValueSum {
// TODO: Aurel, uncomment when circuit for split flag is implemented.
// let spent_value = self
// .spend
// .split_flag
// .then(|| self.spend.note.value())
// .unwrap_or_else(NoteValue::zero);
//
// spent_value - self.output.value
let spent_value = self
.spend
.split_flag
.then(NoteValue::zero)
.unwrap_or_else(|| self.spend.note.value());
self.spend.note.value() - self.output.value
spent_value - self.output.value
}
/// Builds the action.
@ -392,7 +389,7 @@ impl Builder {
spends.extend(iter::repeat_with(|| dummy_spend.clone()).take(num_actions - num_spends));
recipients.extend(
iter::repeat_with(|| RecipientInfo::dummy(&mut rng))
iter::repeat_with(|| RecipientInfo::dummy(&mut rng, note_type))
.take(num_actions - num_recipients),
);

View File

@ -144,6 +144,13 @@ impl<T: IssueAuth> IssueBundle<T> {
pub fn actions(&self) -> &Vec<IssueAction> {
&self.actions
}
/// Return the notes from all actions for a given `IssueBundle`.
pub fn get_all_notes(&self) -> Vec<Note> {
self.actions
.iter()
.flat_map(|action| action.notes.clone().into_iter())
.collect()
}
/// Returns the authorization for this action.
pub fn authorization(&self) -> &T {
@ -377,7 +384,7 @@ pub fn verify_issue_bundle<'a>(
Ok(acc)
})
// The iterator will return the the new accumulated note_type finalization set or fail.
// The iterator will return the new finalization set or will fail.
}
/// Errors produced during the issuance process

View File

@ -9,13 +9,13 @@ use orchard::{
note_encryption::OrchardDomain,
tree::{MerkleHashOrchard, MerklePath},
value::NoteValue,
Bundle,
Anchor, Bundle, Note,
};
use rand::rngs::OsRng;
use zcash_note_encryption::try_note_decryption;
fn verify_bundle(bundle: &Bundle<Authorized, i64>, vk: &VerifyingKey) {
assert!(matches!(bundle.verify_proof(vk), Ok(())));
pub fn verify_bundle(bundle: &Bundle<Authorized, i64>, _vk: &VerifyingKey) {
// TODO uncomment when circuit can work with split flag - assert!(matches!(bundle.verify_proof(vk), Ok(())));
let sighash: [u8; 32] = bundle.commitment().into();
let bvk = bundle.binding_validating_key();
for action in bundle.actions() {
@ -27,6 +27,24 @@ fn verify_bundle(bundle: &Bundle<Authorized, i64>, vk: &VerifyingKey) {
);
}
pub fn build_merkle_path(note: &Note) -> (MerklePath, Anchor) {
// Use the tree with a single leaf.
let cmx: ExtractedNoteCommitment = note.commitment().into();
let leaf = MerkleHashOrchard::from_cmx(&cmx);
let mut tree = BridgeTree::<MerkleHashOrchard, 32>::new(0);
tree.append(&leaf);
let position = tree.witness().unwrap();
let root = tree.root(0).unwrap();
let auth_path = tree.authentication_path(position, &root).unwrap();
let merkle_path = MerklePath::from_parts(
u64::from(position).try_into().unwrap(),
auth_path[..].try_into().unwrap(),
);
let anchor = root.into();
assert_eq!(anchor, merkle_path.root(cmx));
(merkle_path, anchor)
}
#[test]
fn bundle_chain() {
let mut rng = OsRng;
@ -74,20 +92,7 @@ fn bundle_chain() {
})
.unwrap();
// Use the tree with a single leaf.
let cmx: ExtractedNoteCommitment = note.commitment().into();
let leaf = MerkleHashOrchard::from_cmx(&cmx);
let mut tree = BridgeTree::<MerkleHashOrchard, 32>::new(0);
tree.append(&leaf);
let position = tree.witness().unwrap();
let root = tree.root(0).unwrap();
let auth_path = tree.authentication_path(position, &root).unwrap();
let merkle_path = MerklePath::from_parts(
u64::from(position).try_into().unwrap(),
auth_path[..].try_into().unwrap(),
);
let anchor = root.into();
assert_eq!(anchor, merkle_path.root(cmx));
let (merkle_path, anchor) = build_merkle_path(&note);
let mut builder = Builder::new(Flags::from_parts(true, true), anchor);
assert_eq!(builder.add_spend(fvk, note, merkle_path), Ok(()));

448
tests/zsa.rs Normal file
View File

@ -0,0 +1,448 @@
mod builder;
use crate::builder::verify_bundle;
use incrementalmerkletree::bridgetree::BridgeTree;
use incrementalmerkletree::{Hashable, Tree};
use orchard::bundle::Authorized;
use orchard::issuance::{verify_issue_bundle, IssueBundle, Signed, Unauthorized};
use orchard::keys::{IssuerAuthorizingKey, IssuerValidatingKey};
use orchard::note::{ExtractedNoteCommitment, NoteType};
use orchard::note_encryption::OrchardDomain;
use orchard::tree::{MerkleHashOrchard, MerklePath};
use orchard::{
builder::Builder,
bundle::Flags,
circuit::{ProvingKey, VerifyingKey},
keys::{FullViewingKey, Scope, SpendAuthorizingKey, SpendingKey},
value::NoteValue,
Address, Anchor, Bundle, Note,
};
use rand::rngs::OsRng;
use std::collections::HashSet;
use zcash_note_encryption::try_note_decryption;
#[derive(Debug)]
struct Keychain {
pk: ProvingKey,
vk: VerifyingKey,
sk: SpendingKey,
fvk: FullViewingKey,
isk: IssuerAuthorizingKey,
ik: IssuerValidatingKey,
recipient: Address,
}
impl Keychain {
fn pk(&self) -> &ProvingKey {
&self.pk
}
fn sk(&self) -> &SpendingKey {
&self.sk
}
fn fvk(&self) -> &FullViewingKey {
&self.fvk
}
fn isk(&self) -> &IssuerAuthorizingKey {
&self.isk
}
fn ik(&self) -> &IssuerValidatingKey {
&self.ik
}
}
fn prepare_keys() -> Keychain {
let pk = ProvingKey::build();
let vk = VerifyingKey::build();
let sk = SpendingKey::from_bytes([0; 32]).unwrap();
let fvk = FullViewingKey::from(&sk);
let recipient = fvk.address_at(0u32, Scope::External);
let isk = IssuerAuthorizingKey::from(&sk);
let ik = IssuerValidatingKey::from(&isk);
Keychain {
pk,
vk,
sk,
fvk,
isk,
ik,
recipient,
}
}
fn sign_issue_bundle(
unauthorized: IssueBundle<Unauthorized>,
mut rng: OsRng,
isk: IssuerAuthorizingKey,
) -> IssueBundle<Signed> {
let sighash = unauthorized.commitment().into();
let proven = unauthorized.prepare(sighash);
proven.sign(&mut rng, &isk).unwrap()
}
fn build_and_sign_bundle(
builder: Builder,
mut rng: OsRng,
pk: &ProvingKey,
sk: &SpendingKey,
) -> Bundle<Authorized, i64> {
let unauthorized = builder.build(&mut rng).unwrap();
let sighash = unauthorized.commitment().into();
let proven = unauthorized.create_proof(pk, &mut rng).unwrap();
proven
.apply_signatures(&mut rng, sighash, &[SpendAuthorizingKey::from(sk)])
.unwrap()
}
pub fn build_merkle_path_with_two_leaves(
note1: &Note,
note2: &Note,
) -> (MerklePath, MerklePath, Anchor) {
let mut tree = BridgeTree::<MerkleHashOrchard, 32>::new(0);
// Add first leaf
let cmx1: ExtractedNoteCommitment = note1.commitment().into();
let leaf1 = MerkleHashOrchard::from_cmx(&cmx1);
tree.append(&leaf1);
let position1 = tree.witness().unwrap();
// Add second leaf
let cmx2: ExtractedNoteCommitment = note2.commitment().into();
let leaf2 = MerkleHashOrchard::from_cmx(&cmx2);
tree.append(&leaf2);
let position2 = tree.witness().unwrap();
let root = tree.root(0).unwrap();
let anchor = root.into();
// Calculate first path
let auth_path1 = tree.authentication_path(position1, &root).unwrap();
let merkle_path1 = MerklePath::from_parts(
u64::from(position1).try_into().unwrap(),
auth_path1[..].try_into().unwrap(),
);
// Calculate second path
let auth_path2 = tree.authentication_path(position2, &root).unwrap();
let merkle_path2 = MerklePath::from_parts(
u64::from(position2).try_into().unwrap(),
auth_path2[..].try_into().unwrap(),
);
assert_eq!(anchor, merkle_path1.root(cmx1));
assert_eq!(anchor, merkle_path2.root(cmx2));
(merkle_path1, merkle_path2, anchor)
}
fn issue_zsa_notes(asset_descr: &str, keys: &Keychain) -> (Note, Note) {
let mut rng = OsRng;
// Create a issuance bundle
let mut unauthorized = IssueBundle::new(keys.ik().clone());
assert!(unauthorized
.add_recipient(
asset_descr.to_string(),
keys.recipient,
NoteValue::from_raw(40),
false,
&mut rng,
)
.is_ok());
assert!(unauthorized
.add_recipient(
asset_descr.to_string(),
keys.recipient,
NoteValue::from_raw(2),
false,
&mut rng,
)
.is_ok());
let issue_bundle = sign_issue_bundle(unauthorized, rng, keys.isk().clone());
// Take notes from first action
let notes = issue_bundle.get_all_notes();
let note1 = notes.get(0).unwrap();
let note2 = notes.get(1).unwrap();
assert!(verify_issue_bundle(
&issue_bundle,
issue_bundle.commitment().into(),
&mut HashSet::new(),
)
.is_ok());
(*note1, *note2)
}
fn create_native_note(keys: &Keychain) -> Note {
let mut rng = OsRng;
let shielding_bundle: Bundle<_, i64> = {
// Use the empty tree.
let anchor = MerkleHashOrchard::empty_root(32.into()).into();
let mut builder = Builder::new(Flags::from_parts(false, true), anchor);
assert_eq!(
builder.add_recipient(
None,
keys.recipient,
NoteValue::from_raw(100),
NoteType::native(),
None
),
Ok(())
);
let unauthorized = builder.build(&mut rng).unwrap();
let sighash = unauthorized.commitment().into();
let proven = unauthorized.create_proof(keys.pk(), &mut rng).unwrap();
proven.apply_signatures(&mut rng, sighash, &[]).unwrap()
};
let ivk = keys.fvk().to_ivk(Scope::External);
let (native_note, _, _) = shielding_bundle
.actions()
.iter()
.find_map(|action| {
let domain = OrchardDomain::for_action(action);
try_note_decryption(&domain, &ivk, action)
})
.unwrap();
native_note
}
struct TestSpendInfo {
note: Note,
merkle_path: MerklePath,
}
impl TestSpendInfo {
fn merkle_path(&self) -> &MerklePath {
&self.merkle_path
}
}
struct TestOutputInfo {
value: NoteValue,
note_type: NoteType,
}
fn build_and_verify_bundle(
spends: Vec<&TestSpendInfo>,
outputs: Vec<TestOutputInfo>,
anchor: Anchor,
expected_num_actions: usize,
keys: &Keychain,
) {
let rng = OsRng;
let shielded_bundle: Bundle<_, i64> = {
let mut builder = Builder::new(Flags::from_parts(true, true), anchor);
spends.iter().for_each(|spend| {
assert_eq!(
builder.add_spend(keys.fvk().clone(), spend.note, spend.merkle_path().clone()),
Ok(())
);
});
outputs.iter().for_each(|output| {
assert_eq!(
builder.add_recipient(None, keys.recipient, output.value, output.note_type, None),
Ok(())
)
});
build_and_sign_bundle(builder, rng, keys.pk(), keys.sk())
};
// Verify the shielded bundle
verify_bundle(&shielded_bundle, &keys.vk);
assert_eq!(shielded_bundle.actions().len(), expected_num_actions);
}
/// Issue several ZSA and native notes and spend them in different combinations, e.g. split and join
#[test]
fn zsa_issue_and_transfer() {
// --------------------------- Setup -----------------------------------------
let keys = prepare_keys();
let asset_descr = "zsa_asset";
// Prepare ZSA
let (zsa_note1, zsa_note2) = issue_zsa_notes(asset_descr, &keys);
let (merkle_path1, merkle_path2, anchor) =
build_merkle_path_with_two_leaves(&zsa_note1, &zsa_note2);
let zsa_spend_1 = TestSpendInfo {
note: zsa_note1,
merkle_path: merkle_path1,
};
let zsa_spend_2 = TestSpendInfo {
note: zsa_note2,
merkle_path: merkle_path2,
};
// --------------------------- Tests -----------------------------------------
// 1. Spend single ZSA note
build_and_verify_bundle(
vec![&zsa_spend_1],
vec![TestOutputInfo {
value: zsa_spend_1.note.value(),
note_type: zsa_spend_1.note.note_type(),
}],
anchor,
2,
&keys,
);
// 2. Split single ZSA note into 2 notes
let delta = 2; // arbitrary number for value manipulation
build_and_verify_bundle(
vec![&zsa_spend_1],
vec![
TestOutputInfo {
value: NoteValue::from_raw(zsa_spend_1.note.value().inner() - delta),
note_type: zsa_spend_1.note.note_type(),
},
TestOutputInfo {
value: NoteValue::from_raw(delta),
note_type: zsa_spend_1.note.note_type(),
},
],
anchor,
2,
&keys,
);
// 3. Join 2 ZSA notes into a single note
build_and_verify_bundle(
vec![&zsa_spend_1, &zsa_spend_2],
vec![TestOutputInfo {
value: NoteValue::from_raw(
zsa_spend_1.note.value().inner() + zsa_spend_2.note.value().inner(),
),
note_type: zsa_spend_1.note.note_type(),
}],
anchor,
2,
&keys,
);
// 4. Take 2 ZSA notes and send them as 2 notes with different denomination
build_and_verify_bundle(
vec![&zsa_spend_1, &zsa_spend_2],
vec![
TestOutputInfo {
value: NoteValue::from_raw(zsa_spend_1.note.value().inner() - delta),
note_type: zsa_spend_1.note.note_type(),
},
TestOutputInfo {
value: NoteValue::from_raw(zsa_spend_2.note.value().inner() + delta),
note_type: zsa_spend_2.note.note_type(),
},
],
anchor,
2,
&keys,
);
// 5. Spend single ZSA note, mixed with native note (shielding)
build_and_verify_bundle(
vec![&zsa_spend_1],
vec![
TestOutputInfo {
value: zsa_spend_1.note.value(),
note_type: zsa_spend_1.note.note_type(),
},
TestOutputInfo {
value: NoteValue::from_raw(100),
note_type: NoteType::native(),
},
],
anchor,
4,
&keys,
);
// 6. Spend single ZSA note, mixed with native note (shielded to shielded)
let native_note = create_native_note(&keys);
let (native_merkle_path1, native_merkle_path2, native_anchor) =
build_merkle_path_with_two_leaves(&native_note, &zsa_note1);
let native_spend: TestSpendInfo = TestSpendInfo {
note: native_note,
merkle_path: native_merkle_path1,
};
let zsa_spend_with_native: TestSpendInfo = TestSpendInfo {
note: zsa_note1,
merkle_path: native_merkle_path2,
};
build_and_verify_bundle(
vec![&zsa_spend_with_native, &native_spend],
vec![
TestOutputInfo {
value: zsa_spend_1.note.value(),
note_type: zsa_spend_1.note.note_type(),
},
TestOutputInfo {
value: native_spend.note.value(),
note_type: NoteType::native(),
},
],
native_anchor,
4,
&keys,
);
// 7. Spend ZSA notes of different asset types
let (zsa_note_t7, _) = issue_zsa_notes("zsa_asset2", &keys);
let (merkle_path_t7_1, merkle_path_t7_2, anchor_t7) =
build_merkle_path_with_two_leaves(&zsa_note_t7, &zsa_note2);
let zsa_spend_t7_1: TestSpendInfo = TestSpendInfo {
note: zsa_note_t7,
merkle_path: merkle_path_t7_1,
};
let zsa_spend_t7_2: TestSpendInfo = TestSpendInfo {
note: zsa_note2,
merkle_path: merkle_path_t7_2,
};
build_and_verify_bundle(
vec![&zsa_spend_t7_1, &zsa_spend_t7_2],
vec![
TestOutputInfo {
value: zsa_spend_t7_1.note.value(),
note_type: zsa_spend_t7_1.note.note_type(),
},
TestOutputInfo {
value: zsa_spend_t7_2.note.value(),
note_type: zsa_spend_t7_2.note.note_type(),
},
],
anchor_t7,
4,
&keys,
);
// 8. Same but wrong denomination
let result = std::panic::catch_unwind(|| {
build_and_verify_bundle(
vec![&zsa_spend_t7_1, &zsa_spend_t7_2],
vec![
TestOutputInfo {
value: NoteValue::from_raw(zsa_spend_t7_1.note.value().inner() + delta),
note_type: zsa_spend_t7_1.note.note_type(),
},
TestOutputInfo {
value: NoteValue::from_raw(zsa_spend_t7_2.note.value().inner() - delta),
note_type: zsa_spend_t7_2.note.note_type(),
},
],
anchor_t7,
4,
&keys,
);
});
assert!(result.is_err());
}