diff --git a/zebra-chain/src/lib.rs b/zebra-chain/src/lib.rs index a68a1127014..87eec1b40e3 100644 --- a/zebra-chain/src/lib.rs +++ b/zebra-chain/src/lib.rs @@ -7,6 +7,7 @@ mod sha256d_writer; pub mod block; pub mod equihash_solution; pub mod note_commitment_tree; +pub mod note_encryption; pub mod proofs; pub mod serialization; pub mod transaction; diff --git a/zebra-chain/src/note_encryption.rs b/zebra-chain/src/note_encryption.rs new file mode 100644 index 00000000000..86942a350b2 --- /dev/null +++ b/zebra-chain/src/note_encryption.rs @@ -0,0 +1,4 @@ +//! Note encryption types. +mod memo; +mod sapling; +mod sprout; diff --git a/zebra-chain/src/note_encryption/memo.rs b/zebra-chain/src/note_encryption/memo.rs new file mode 100644 index 00000000000..49c34ee12de --- /dev/null +++ b/zebra-chain/src/note_encryption/memo.rs @@ -0,0 +1,83 @@ +use std::{cmp, convert::TryFrom, fmt}; + +/// A 512-byte _Memo_ field associated with a note, as described in +/// [protocol specification §5.5][ps]. +/// +/// The _Memo_ field of a note is a plaintext type; the parent note is +/// what is encrypted and stored on the blockchain. The underlying +/// usage of the memo field is by agreement between the sender and +/// recipient of the note. +/// +/// [ps]: https://zips.z.cash/protocol/protocol.pdf#notept +#[derive(Clone)] +pub struct Memo(Box<[u8; 512]>); + +impl<'a> TryFrom<&'a [u8]> for Memo { + type Error = &'static str; + + fn try_from(input: &'a [u8]) -> Result { + let mut full_bytes = [0; 512]; + + match input.len().cmp(&512) { + cmp::Ordering::Less => { + full_bytes[0..input.len()].copy_from_slice(input); + Ok(Memo(Box::new(full_bytes))) + } + cmp::Ordering::Equal => { + full_bytes[..].copy_from_slice(input); + Ok(Memo(Box::new(full_bytes))) + } + cmp::Ordering::Greater => Err("Memos have a max length of 512 bytes."), + } + } +} + +impl fmt::Debug for Memo { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let output: String; + + // This saves work but if the 'valid utf8 string' is just a + // bunch of numbers, it prints them out like + // 'Memo("\u{0}\u{0}..")', so. ¯\_(ツ)_/¯ + match std::str::from_utf8(&self.0[..]) { + Ok(memo) => output = String::from(memo), + _ => output = hex::encode(&self.0[..]), + } + + f.debug_tuple("Memo").field(&output).finish() + } +} + +#[test] +fn memo_fmt() { + let memo = Memo(Box::new( + *b"thiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiis \ + iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiis \ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ + veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeryyyyyyyyyyyyyyyyyyyyyyyyyy \ + looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong \ + meeeeeeeeeeeeeeeeeeemooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo \ + but it's just short enough", + )); + + assert_eq!(format!("{:?}", memo), + "Memo(\"thiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiis iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiis aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeryyyyyyyyyyyyyyyyyyyyyyyyyy looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong meeeeeeeeeeeeeeeeeeemooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo but it\\\'s just short enough\")" + ); + + let mut some_bytes = [0u8; 512]; + some_bytes[0] = 0xF6; + + assert_eq!(format!("{:?}", Memo(Box::new(some_bytes))), + "Memo(\"f600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\")" + ); +} + +#[test] +fn memo_from_string() { + let memo = Memo::try_from("foo bar baz".as_ref()).unwrap(); + + let mut bytes = [0; 512]; + bytes[0..11].copy_from_slice(&[102, 111, 111, 32, 98, 97, 114, 32, 98, 97, 122]); + + assert!(memo.0.iter().eq(bytes.iter())); +} diff --git a/zebra-chain/src/note_encryption/sapling.rs b/zebra-chain/src/note_encryption/sapling.rs new file mode 100644 index 00000000000..9fcb5efeace --- /dev/null +++ b/zebra-chain/src/note_encryption/sapling.rs @@ -0,0 +1,160 @@ +use std::{fmt, io}; + +#[cfg(test)] +use proptest::{arbitrary::Arbitrary, collection::vec, prelude::*}; + +use crate::serialization::{SerializationError, ZcashDeserialize, ZcashSerialize}; + +/// A ciphertext component for encrypted output notes. +pub struct EncryptedCiphertext(pub [u8; 580]); + +impl fmt::Debug for EncryptedCiphertext { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("EncryptedCiphertext") + .field(&hex::encode(&self.0[..])) + .finish() + } +} + +// These impls all only exist because of array length restrictions. + +impl Copy for EncryptedCiphertext {} + +impl Clone for EncryptedCiphertext { + fn clone(&self) -> Self { + let mut bytes = [0; 580]; + bytes[..].copy_from_slice(&self.0[..]); + Self(bytes) + } +} + +impl PartialEq for EncryptedCiphertext { + fn eq(&self, other: &Self) -> bool { + self.0[..] == other.0[..] + } +} + +impl Eq for EncryptedCiphertext {} + +impl ZcashSerialize for EncryptedCiphertext { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + writer.write_all(&self.0[..])?; + Ok(()) + } +} + +impl ZcashDeserialize for EncryptedCiphertext { + fn zcash_deserialize(mut reader: R) -> Result { + let mut bytes = [0; 580]; + reader.read_exact(&mut bytes[..])?; + Ok(Self(bytes)) + } +} + +#[cfg(test)] +impl Arbitrary for EncryptedCiphertext { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (vec(any::(), 580)) + .prop_map(|v| { + let mut bytes = [0; 580]; + bytes.copy_from_slice(v.as_slice()); + return Self(bytes); + }) + .boxed() + } + + type Strategy = BoxedStrategy; +} + +/// A ciphertext component for encrypted output notes. +pub struct OutCiphertext(pub [u8; 80]); + +impl fmt::Debug for OutCiphertext { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("OutCiphertext") + .field(&hex::encode(&self.0[..])) + .finish() + } +} + +// These impls all only exist because of array length restrictions. + +impl Copy for OutCiphertext {} + +impl Clone for OutCiphertext { + fn clone(&self) -> Self { + let mut bytes = [0; 80]; + bytes[..].copy_from_slice(&self.0[..]); + Self(bytes) + } +} + +impl PartialEq for OutCiphertext { + fn eq(&self, other: &Self) -> bool { + self.0[..] == other.0[..] + } +} + +impl Eq for OutCiphertext {} + +impl ZcashSerialize for OutCiphertext { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + writer.write_all(&self.0[..])?; + Ok(()) + } +} + +impl ZcashDeserialize for OutCiphertext { + fn zcash_deserialize(mut reader: R) -> Result { + let mut bytes = [0; 80]; + reader.read_exact(&mut bytes[..])?; + Ok(Self(bytes)) + } +} + +#[cfg(test)] +impl Arbitrary for OutCiphertext { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (vec(any::(), 80)) + .prop_map(|v| { + let mut bytes = [0; 80]; + bytes.copy_from_slice(v.as_slice()); + return Self(bytes); + }) + .boxed() + } + + type Strategy = BoxedStrategy; +} + +#[cfg(test)] +proptest! { + + #[test] + fn encrypted_ciphertext_roundtrip(ec in any::()) { + + let mut data = Vec::new(); + + ec.zcash_serialize(&mut data).expect("EncryptedCiphertext should serialize"); + + let ec2 = EncryptedCiphertext::zcash_deserialize(&data[..]).expect("randomized EncryptedCiphertext should deserialize"); + + prop_assert_eq![ec, ec2]; + } + + #[test] + fn out_ciphertext_roundtrip(oc in any::()) { + + let mut data = Vec::new(); + + oc.zcash_serialize(&mut data).expect("OutCiphertext should serialize"); + + let oc2 = OutCiphertext::zcash_deserialize(&data[..]).expect("randomized OutCiphertext should deserialize"); + + prop_assert_eq![oc, oc2]; + } +} diff --git a/zebra-chain/src/note_encryption/sprout.rs b/zebra-chain/src/note_encryption/sprout.rs new file mode 100644 index 00000000000..cc3b12dd0fa --- /dev/null +++ b/zebra-chain/src/note_encryption/sprout.rs @@ -0,0 +1,88 @@ +use std::{ + fmt, + io::{self}, +}; + +#[cfg(test)] +use proptest::{collection::vec, prelude::*}; + +use crate::serialization::{SerializationError, ZcashDeserialize, ZcashSerialize}; + +/// A ciphertext component for encrypted output notes. +pub struct EncryptedCiphertext(pub [u8; 601]); + +impl fmt::Debug for EncryptedCiphertext { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("EncryptedCiphertext") + .field(&hex::encode(&self.0[..])) + .finish() + } +} + +// These impls all only exist because of array length restrictions. + +impl Copy for EncryptedCiphertext {} + +impl Clone for EncryptedCiphertext { + fn clone(&self) -> Self { + let mut bytes = [0; 601]; + bytes[..].copy_from_slice(&self.0[..]); + Self(bytes) + } +} + +impl PartialEq for EncryptedCiphertext { + fn eq(&self, other: &Self) -> bool { + self.0[..] == other.0[..] + } +} + +impl Eq for EncryptedCiphertext {} + +impl ZcashSerialize for EncryptedCiphertext { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + writer.write_all(&self.0[..])?; + Ok(()) + } +} + +impl ZcashDeserialize for EncryptedCiphertext { + fn zcash_deserialize(mut reader: R) -> Result { + let mut bytes = [0; 601]; + reader.read_exact(&mut bytes[..])?; + Ok(Self(bytes)) + } +} + +#[cfg(test)] +impl Arbitrary for EncryptedCiphertext { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + (vec(any::(), 601)) + .prop_map(|v| { + let mut bytes = [0; 601]; + bytes.copy_from_slice(v.as_slice()); + return Self(bytes); + }) + .boxed() + } + + type Strategy = BoxedStrategy; +} + +#[cfg(test)] +proptest! { + + #[test] + fn encrypted_ciphertext_roundtrip(ec in any::()) { + + let mut data = Vec::new(); + + ec.zcash_serialize(&mut data).expect("EncryptedCiphertext should serialize"); + + let ec2 = EncryptedCiphertext::zcash_deserialize(&data[..]).expect("randomized EncryptedCiphertext should deserialize"); + + prop_assert_eq![ec, ec2]; + } +} diff --git a/zebra-chain/src/note_encryption/test_vectors.rs b/zebra-chain/src/note_encryption/test_vectors.rs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/zebra-chain/src/note_encryption/tests.rs b/zebra-chain/src/note_encryption/tests.rs new file mode 100644 index 00000000000..e69de29bb2d