mirror of https://github.com/zcash/orchard.git
559 lines
16 KiB
Rust
559 lines
16 KiB
Rust
mod builder;
|
|
|
|
use crate::builder::verify_bundle;
|
|
use bridgetree::BridgeTree;
|
|
use incrementalmerkletree::Hashable;
|
|
use orchard::bundle::Authorized;
|
|
use orchard::issuance::{verify_issue_bundle, IssueBundle, IssueInfo, Signed, Unauthorized};
|
|
use orchard::keys::{IssuanceAuthorizingKey, IssuanceValidatingKey};
|
|
use orchard::note::{AssetBase, ExtractedNoteCommitment};
|
|
use orchard::note_encryption_v3::OrchardDomainV3;
|
|
use orchard::tree::{MerkleHashOrchard, MerklePath};
|
|
use orchard::{
|
|
builder::Builder,
|
|
bundle::Flags,
|
|
circuit::{ProvingKey, VerifyingKey},
|
|
keys::{FullViewingKey, PreparedIncomingViewingKey, Scope, SpendAuthorizingKey, SpendingKey},
|
|
value::NoteValue,
|
|
Address, Anchor, Bundle, Note,
|
|
};
|
|
use rand::rngs::OsRng;
|
|
use std::collections::HashSet;
|
|
use zcash_note_encryption_zsa::try_note_decryption;
|
|
|
|
#[derive(Debug)]
|
|
struct Keychain {
|
|
pk: ProvingKey,
|
|
vk: VerifyingKey,
|
|
sk: SpendingKey,
|
|
fvk: FullViewingKey,
|
|
isk: IssuanceAuthorizingKey,
|
|
ik: IssuanceValidatingKey,
|
|
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) -> &IssuanceAuthorizingKey {
|
|
&self.isk
|
|
}
|
|
fn ik(&self) -> &IssuanceValidatingKey {
|
|
&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 = IssuanceAuthorizingKey::from_bytes([1u8; 32]).unwrap();
|
|
let ik = IssuanceValidatingKey::from(&isk);
|
|
Keychain {
|
|
pk,
|
|
vk,
|
|
sk,
|
|
fvk,
|
|
isk,
|
|
ik,
|
|
recipient,
|
|
}
|
|
}
|
|
|
|
fn sign_issue_bundle(
|
|
unauthorized: IssueBundle<Unauthorized>,
|
|
isk: &IssuanceAuthorizingKey,
|
|
) -> IssueBundle<Signed> {
|
|
let sighash = unauthorized.commitment().into();
|
|
let proven = unauthorized.prepare(sighash);
|
|
proven.sign(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(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, u32, 32>::new(100);
|
|
|
|
// Add first leaf
|
|
let cmx1: ExtractedNoteCommitment = note1.commitment().into();
|
|
let leaf1 = MerkleHashOrchard::from_cmx(&cmx1);
|
|
tree.append(leaf1);
|
|
let position1 = tree.mark().unwrap();
|
|
|
|
// Add second leaf
|
|
let cmx2: ExtractedNoteCommitment = note2.commitment().into();
|
|
let leaf2 = MerkleHashOrchard::from_cmx(&cmx2);
|
|
tree.append(leaf2);
|
|
let position2 = tree.mark().unwrap();
|
|
|
|
let root = tree.root(0).unwrap();
|
|
let anchor = root.into();
|
|
|
|
// Calculate first path
|
|
let auth_path1 = tree.witness(position1, 0).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.witness(position2, 0).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 unauthorized_asset = IssueBundle::new(
|
|
keys.ik().clone(),
|
|
asset_descr.to_string(),
|
|
Some(IssueInfo {
|
|
recipient: keys.recipient,
|
|
value: NoteValue::from_raw(40),
|
|
}),
|
|
&mut rng,
|
|
);
|
|
|
|
assert!(unauthorized_asset.is_ok());
|
|
|
|
let (mut unauthorized, _) = unauthorized_asset.unwrap();
|
|
|
|
assert!(unauthorized
|
|
.add_recipient(
|
|
asset_descr.to_string(),
|
|
keys.recipient,
|
|
NoteValue::from_raw(2),
|
|
&mut rng,
|
|
)
|
|
.is_ok());
|
|
|
|
let issue_bundle = sign_issue_bundle(unauthorized, keys.isk());
|
|
|
|
// 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(),
|
|
&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, false), anchor);
|
|
assert_eq!(
|
|
builder.add_recipient(
|
|
None,
|
|
keys.recipient,
|
|
NoteValue::from_raw(100),
|
|
AssetBase::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(rng, sighash, &[]).unwrap()
|
|
};
|
|
let ivk = keys.fvk().to_ivk(Scope::External);
|
|
let (native_note, _, _) = shielding_bundle
|
|
.actions()
|
|
.iter()
|
|
.find_map(|action| {
|
|
let domain = OrchardDomainV3::for_action(action);
|
|
try_note_decryption(&domain, &PreparedIncomingViewingKey::new(&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,
|
|
asset: AssetBase,
|
|
}
|
|
|
|
fn build_and_verify_bundle(
|
|
spends: Vec<&TestSpendInfo>,
|
|
outputs: Vec<TestOutputInfo>,
|
|
assets_to_burn: Vec<(AssetBase, NoteValue)>,
|
|
anchor: Anchor,
|
|
expected_num_actions: usize,
|
|
keys: &Keychain,
|
|
) -> Result<(), String> {
|
|
let rng = OsRng;
|
|
let shielded_bundle: Bundle<_, i64> = {
|
|
let mut builder = Builder::new(Flags::from_parts(true, true, true), anchor);
|
|
|
|
spends
|
|
.iter()
|
|
.try_for_each(|spend| {
|
|
builder.add_spend(keys.fvk().clone(), spend.note, spend.merkle_path().clone())
|
|
})
|
|
.map_err(|err| err.to_string())?;
|
|
outputs
|
|
.iter()
|
|
.try_for_each(|output| {
|
|
builder.add_recipient(None, keys.recipient, output.value, output.asset, None)
|
|
})
|
|
.map_err(|err| err.to_string())?;
|
|
assets_to_burn
|
|
.into_iter()
|
|
.try_for_each(|(asset, value)| builder.add_burn(asset, value))?;
|
|
build_and_sign_bundle(builder, rng, keys.pk(), keys.sk())
|
|
};
|
|
|
|
// Verify the shielded bundle, currently without the proof.
|
|
verify_bundle(&shielded_bundle, &keys.vk, true);
|
|
assert_eq!(shielded_bundle.actions().len(), expected_num_actions);
|
|
assert!(verify_unique_spent_nullifiers(&shielded_bundle));
|
|
Ok(())
|
|
}
|
|
|
|
fn verify_unique_spent_nullifiers(bundle: &Bundle<Authorized, i64>) -> bool {
|
|
let mut unique_nulifiers = Vec::new();
|
|
let spent_nullifiers = bundle
|
|
.actions()
|
|
.iter()
|
|
.map(|action| *action.nullifier())
|
|
.collect::<Vec<_>>();
|
|
spent_nullifiers.iter().enumerate().all(|(i, item)| {
|
|
unique_nulifiers.push(*item);
|
|
// Check if the item is already in the unique_nullifiers vector by checking that the first
|
|
// position of the item is equal to the current index i.
|
|
unique_nulifiers.iter().position(|x| x == item) == Some(i)
|
|
})
|
|
}
|
|
|
|
/// 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_note_1, zsa_note_2) = issue_zsa_notes(asset_descr, &keys);
|
|
|
|
let (merkle_path1, merkle_path2, anchor) =
|
|
build_merkle_path_with_two_leaves(&zsa_note_1, &zsa_note_2);
|
|
|
|
let zsa_spend_1 = TestSpendInfo {
|
|
note: zsa_note_1,
|
|
merkle_path: merkle_path1,
|
|
};
|
|
let zsa_spend_2 = TestSpendInfo {
|
|
note: zsa_note_2,
|
|
merkle_path: merkle_path2,
|
|
};
|
|
|
|
let native_note = create_native_note(&keys);
|
|
let (native_merkle_path_1, native_merkle_path_2, native_anchor) =
|
|
build_merkle_path_with_two_leaves(&native_note, &zsa_note_1);
|
|
let native_spend: TestSpendInfo = TestSpendInfo {
|
|
note: native_note,
|
|
merkle_path: native_merkle_path_1,
|
|
};
|
|
let zsa_spend_with_native: TestSpendInfo = TestSpendInfo {
|
|
note: zsa_note_1,
|
|
merkle_path: native_merkle_path_2,
|
|
};
|
|
|
|
// --------------------------- Tests -----------------------------------------
|
|
|
|
// 1. Spend single ZSA note
|
|
build_and_verify_bundle(
|
|
vec![&zsa_spend_1],
|
|
vec![TestOutputInfo {
|
|
value: zsa_spend_1.note.value(),
|
|
asset: zsa_spend_1.note.asset(),
|
|
}],
|
|
vec![],
|
|
anchor,
|
|
2,
|
|
&keys,
|
|
)
|
|
.unwrap();
|
|
|
|
// 2. Split single ZSA note into 3 notes
|
|
let delta_1 = 2; // arbitrary number for value manipulation
|
|
let delta_2 = 5; // 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_1 - delta_2),
|
|
asset: zsa_spend_1.note.asset(),
|
|
},
|
|
TestOutputInfo {
|
|
value: NoteValue::from_raw(delta_1),
|
|
asset: zsa_spend_1.note.asset(),
|
|
},
|
|
TestOutputInfo {
|
|
value: NoteValue::from_raw(delta_2),
|
|
asset: zsa_spend_1.note.asset(),
|
|
},
|
|
],
|
|
vec![],
|
|
anchor,
|
|
3,
|
|
&keys,
|
|
)
|
|
.unwrap();
|
|
|
|
// 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(),
|
|
),
|
|
asset: zsa_spend_1.note.asset(),
|
|
}],
|
|
vec![],
|
|
anchor,
|
|
2,
|
|
&keys,
|
|
)
|
|
.unwrap();
|
|
|
|
// 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_1),
|
|
asset: zsa_spend_1.note.asset(),
|
|
},
|
|
TestOutputInfo {
|
|
value: NoteValue::from_raw(zsa_spend_2.note.value().inner() + delta_1),
|
|
asset: zsa_spend_2.note.asset(),
|
|
},
|
|
],
|
|
vec![],
|
|
anchor,
|
|
2,
|
|
&keys,
|
|
)
|
|
.unwrap();
|
|
|
|
// 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(),
|
|
asset: zsa_spend_1.note.asset(),
|
|
},
|
|
TestOutputInfo {
|
|
value: NoteValue::from_raw(100),
|
|
asset: AssetBase::native(),
|
|
},
|
|
],
|
|
vec![],
|
|
anchor,
|
|
2,
|
|
&keys,
|
|
)
|
|
.unwrap();
|
|
|
|
// 6. Spend single ZSA note, mixed with native note (shielded to shielded)
|
|
build_and_verify_bundle(
|
|
vec![&zsa_spend_with_native, &native_spend],
|
|
vec![
|
|
TestOutputInfo {
|
|
value: zsa_spend_1.note.value(),
|
|
asset: zsa_spend_1.note.asset(),
|
|
},
|
|
TestOutputInfo {
|
|
value: NoteValue::from_raw(native_spend.note.value().inner() - delta_1 - delta_2),
|
|
asset: AssetBase::native(),
|
|
},
|
|
TestOutputInfo {
|
|
value: NoteValue::from_raw(delta_1),
|
|
asset: AssetBase::native(),
|
|
},
|
|
TestOutputInfo {
|
|
value: NoteValue::from_raw(delta_2),
|
|
asset: AssetBase::native(),
|
|
},
|
|
],
|
|
vec![],
|
|
native_anchor,
|
|
4,
|
|
&keys,
|
|
)
|
|
.unwrap();
|
|
|
|
// 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_note_2);
|
|
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_note_2,
|
|
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(),
|
|
asset: zsa_spend_t7_1.note.asset(),
|
|
},
|
|
TestOutputInfo {
|
|
value: zsa_spend_t7_2.note.value(),
|
|
asset: zsa_spend_t7_2.note.asset(),
|
|
},
|
|
],
|
|
vec![],
|
|
anchor_t7,
|
|
2,
|
|
&keys,
|
|
)
|
|
.unwrap();
|
|
|
|
// 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_1),
|
|
asset: zsa_spend_t7_1.note.asset(),
|
|
},
|
|
TestOutputInfo {
|
|
value: NoteValue::from_raw(zsa_spend_t7_2.note.value().inner() - delta_1),
|
|
asset: zsa_spend_t7_2.note.asset(),
|
|
},
|
|
],
|
|
vec![],
|
|
anchor_t7,
|
|
2,
|
|
&keys,
|
|
)
|
|
.unwrap();
|
|
});
|
|
assert!(result.is_err());
|
|
|
|
// 9. Burn ZSA assets
|
|
build_and_verify_bundle(
|
|
vec![&zsa_spend_1],
|
|
vec![],
|
|
vec![(zsa_spend_1.note.asset(), zsa_spend_1.note.value())],
|
|
anchor,
|
|
2,
|
|
&keys,
|
|
)
|
|
.unwrap();
|
|
|
|
// 10. Burn a partial amount of the ZSA assets
|
|
let value_to_burn = 3;
|
|
let value_to_transfer = zsa_spend_1.note.value().inner() - value_to_burn;
|
|
|
|
build_and_verify_bundle(
|
|
vec![&zsa_spend_1],
|
|
vec![TestOutputInfo {
|
|
value: NoteValue::from_raw(value_to_transfer),
|
|
asset: zsa_spend_1.note.asset(),
|
|
}],
|
|
vec![(zsa_spend_1.note.asset(), NoteValue::from_raw(value_to_burn))],
|
|
anchor,
|
|
2,
|
|
&keys,
|
|
)
|
|
.unwrap();
|
|
|
|
// 11. Try to burn native asset - should fail
|
|
let result = build_and_verify_bundle(
|
|
vec![&native_spend],
|
|
vec![],
|
|
vec![(AssetBase::native(), native_spend.note.value())],
|
|
native_anchor,
|
|
2,
|
|
&keys,
|
|
);
|
|
match result {
|
|
Ok(_) => panic!("Test should fail"),
|
|
Err(error) => assert_eq!(error, "Burning is only possible for non-native assets"),
|
|
}
|
|
|
|
// 12. Try to burn zero value - should fail
|
|
let result = build_and_verify_bundle(
|
|
vec![&zsa_spend_1],
|
|
vec![TestOutputInfo {
|
|
value: zsa_spend_1.note.value(),
|
|
asset: zsa_spend_1.note.asset(),
|
|
}],
|
|
vec![(zsa_spend_1.note.asset(), NoteValue::from_raw(0))],
|
|
anchor,
|
|
2,
|
|
&keys,
|
|
);
|
|
match result {
|
|
Ok(_) => panic!("Test should fail"),
|
|
Err(error) => assert_eq!(error, "Burning is not possible for zero values"),
|
|
}
|
|
}
|