From 2168ab358ed1641776e39efa0dda0d159d18332b Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Thu, 28 Nov 2019 13:54:41 +0000 Subject: [PATCH 1/2] ZIP 302 structured memos --- Cargo.lock | 10 + components/zcash_protocol/Cargo.toml | 1 + components/zcash_protocol/src/memo.rs | 16 ++ .../zcash_protocol/src/memo/structured.rs | 270 ++++++++++++++++++ 4 files changed, 297 insertions(+) create mode 100644 components/zcash_protocol/src/memo/structured.rs diff --git a/Cargo.lock b/Cargo.lock index 7dea9d0859..948a656cf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5965,6 +5965,15 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasabi_leb128" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef95b2fc26dab4b706f4763b297aa7277a1660e83450fa39e6832e64aeeb637" +dependencies = [ + "num-traits", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -6714,6 +6723,7 @@ dependencies = [ "incrementalmerkletree-testing", "memuse", "proptest", + "wasabi_leb128", ] [[package]] diff --git a/components/zcash_protocol/Cargo.toml b/components/zcash_protocol/Cargo.toml index 9fb2928eca..2f8b01eeca 100644 --- a/components/zcash_protocol/Cargo.toml +++ b/components/zcash_protocol/Cargo.toml @@ -31,6 +31,7 @@ document-features = { workspace = true, optional = true } # - Encodings core2.workspace = true hex.workspace = true +wasabi_leb128 = "0.4" # - Test dependencies proptest = { workspace = true, optional = true } diff --git a/components/zcash_protocol/src/memo.rs b/components/zcash_protocol/src/memo.rs index f48aff55ea..bf1a8b8c40 100644 --- a/components/zcash_protocol/src/memo.rs +++ b/components/zcash_protocol/src/memo.rs @@ -11,6 +11,9 @@ use core::str; #[cfg(feature = "std")] use std::error; +mod structured; +pub use structured::{Payload, StructuredMemo}; + /// Format a byte array as a colon-delimited hex string. /// /// - Source: @@ -35,6 +38,7 @@ where /// Errors that may result from attempting to construct an invalid memo. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Error { + InvalidEncoding, InvalidUtf8(core::str::Utf8Error), TooLong(usize), } @@ -42,6 +46,7 @@ pub enum Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Error::InvalidEncoding => write!(f, "Invalid memo encoding"), Error::InvalidUtf8(e) => write!(f, "Invalid UTF-8: {e}"), Error::TooLong(n) => write!(f, "Memo length {n} is larger than maximum of 512"), } @@ -162,6 +167,8 @@ pub enum Memo { Empty, /// A memo field containing a UTF-8 string. Text(TextMemo), + /// A structured memo field containing one or more payloads. + Structured(StructuredMemo), /// Some unknown memo format from ✨*the future*✨ that we can't parse. Future(MemoBytes), /// A memo field containing arbitrary bytes. @@ -173,6 +180,7 @@ impl fmt::Debug for Memo { match self { Memo::Empty => write!(f, "Memo::Empty"), Memo::Text(memo) => write!(f, "Memo::Text(\"{}\")", memo.0), + Memo::Structured(memo) => write!(f, "Memo::Structured({:?})", memo), Memo::Future(bytes) => write!(f, "Memo::Future({:0x})", bytes.0[0]), Memo::Arbitrary(bytes) => { write!(f, "Memo::Arbitrary(")?; @@ -188,6 +196,7 @@ impl PartialEq for Memo { match (self, rhs) { (Memo::Empty, Memo::Empty) => true, (Memo::Text(a), Memo::Text(b)) => a == b, + (Memo::Structured(a), Memo::Structured(b)) => a == b, (Memo::Future(a), Memo::Future(b)) => a.0[..] == b.0[..], (Memo::Arbitrary(a), Memo::Arbitrary(b)) => a[..] == b[..], _ => false, @@ -216,6 +225,7 @@ impl TryFrom<&MemoBytes> for Memo { /// example, if the slice is not 512 bytes, or the encoded `Memo` is non-canonical). fn try_from(bytes: &MemoBytes) -> Result { match bytes.0[0] { + 0xF5 => StructuredMemo::parse(&bytes.0[1..]).map(Memo::Structured), 0xF6 if bytes.0.iter().skip(1).all(|&b| b == 0) => Ok(Memo::Empty), 0xFF => Ok(Memo::Arbitrary(Box::new(bytes.0[1..].try_into().unwrap()))), b if b <= 0xF4 => str::from_utf8(bytes.as_slice()) @@ -249,6 +259,12 @@ impl From<&Memo> for MemoBytes { bytes[..s_bytes.len()].copy_from_slice(s_bytes); MemoBytes(Box::new(bytes)) } + Memo::Structured(s) => { + let mut bytes = [0u8; 512]; + bytes[0] = 0xF5; + s.serialize(&mut bytes[1..]); + MemoBytes(Box::new(bytes)) + } Memo::Future(memo) => memo.clone(), Memo::Arbitrary(arb) => { let mut bytes = [0u8; 512]; diff --git a/components/zcash_protocol/src/memo/structured.rs b/components/zcash_protocol/src/memo/structured.rs new file mode 100644 index 0000000000..f254f7cabc --- /dev/null +++ b/components/zcash_protocol/src/memo/structured.rs @@ -0,0 +1,270 @@ +//! Handlers for structured memos. + +use alloc::vec::Vec; + +use wasabi_leb128::{ReadLeb128, WriteLeb128}; + +use super::Error; + +/// A payload within a [`StructuredMemo`]. +#[derive(Clone, Debug, PartialEq)] +pub enum Payload { + /// A payload type we don't know about. + Unknown { + type_id: u64, + length: u16, + value: Vec, + }, +} + +impl Payload { + /// Parses a `Payload` from memo field bytes. + fn parse(mut bytes: &[u8]) -> Result<(&[u8], Option), Error> { + match bytes.read_leb128().map_err(|_| Error::InvalidEncoding)? { + (0x00, _) => Ok((bytes, None)), + (type_id, _) => { + let (length, _): (u16, _) = + bytes.read_leb128().map_err(|_| Error::InvalidEncoding)?; + if length as usize > bytes.len() { + return Err(Error::InvalidEncoding); + } + + let (value, rem) = bytes.split_at(length as usize); + + Ok(( + rem, + Some(Payload::Unknown { + type_id, + length, + value: value.into(), + }), + )) + } + } + } + + /// Returns the number of memo field bytes that this `Payload` will occupy. + fn serialized_len(&self) -> usize { + let mut buf = [0; wasabi_leb128::max_bytes::()]; + match self { + Payload::Unknown { + type_id, length, .. + } => { + let type_len = (&mut buf[..]) + .write_leb128(*type_id) + .expect("buffer is large enough"); + let length_len = (&mut buf[..]) + .write_leb128(*length) + .expect("buffer is large enough"); + type_len + length_len + *length as usize + } + } + } + + /// Serializes this `Payload` into the provided buffer. + /// + /// Panics if `buf` is not large enough. Caller must verify that this `Payload` will + /// fit into `buf` by checking `serialized_len`. + fn serialize(&self, mut buf: &mut [u8]) -> usize { + match self { + Payload::Unknown { + type_id, + length, + value, + } => { + let mut written = buf.write_leb128(*type_id).expect("buffer is large enough"); + written += buf.write_leb128(*length).expect("buffer is large enough"); + let length = *length as usize; + buf[..length].copy_from_slice(&value[..length]); + written + length + } + } + } +} + +/// A structured [`Memo`] that contains at least one [`Payload`]. +/// +/// [`Memo`]: crate::memo::Memo +#[derive(Clone, Debug, PartialEq)] +pub struct StructuredMemo(Vec); + +impl AsRef<[Payload]> for StructuredMemo { + fn as_ref(&self) -> &[Payload] { + &self.0 + } +} + +impl StructuredMemo { + /// Pack a set of [`Payload`]s into a `StructuredMemo`. + /// + /// Returns an error if `payloads` is empty or will not fit into a memo field. + pub fn new(payloads: Vec) -> Result { + if payloads.is_empty() || payloads.iter().map(|p| p.serialized_len()).sum::() > 511 { + Err(()) + } else { + Ok(StructuredMemo(payloads)) + } + } + + /// Parses a `StructuredMemo` from the its ZIP 302 serialization. + pub(super) fn parse(mut bytes: &[u8]) -> Result { + // Internal function, this invariant should always hold. + assert_eq!(bytes.len(), 511); + + let mut payloads = Vec::new(); + + loop { + // Parse the next payload + bytes = match Payload::parse(bytes)? { + (c, Some(payload)) => { + payloads.push(payload); + + if c.is_empty() { + // Finished parsing! + break; + } + + // There may be more payloads + c + } + (c, None) => { + // Remainder of bytes should be padding + for b in c { + if *b != 0x00 { + return Err(Error::InvalidEncoding); + } + } + + // Finished parsing! + break; + } + }; + } + + if payloads.is_empty() { + // Non-canonical empty memo, should be using Memo::Empty + Err(Error::InvalidEncoding) + } else { + Ok(StructuredMemo(payloads)) + } + } + + /// Serializes the `StructuredMemo` per ZIP 302. + pub(super) fn serialize(&self, mut buf: &mut [u8]) { + // Internal function, this invariant should always hold. + assert_eq!(buf.len(), 511); + + // A `StructuredMemo` can only be constructed such that its payloads are + // guaranteed to fit in `buf`. + for payload in &self.0 { + let written = payload.serialize(buf); + buf = &mut buf[written..]; + } + + // Ensure that remaining buffer is padding with zeroes + for b in buf.iter_mut() { + *b = 0; + } + } +} + +#[cfg(test)] +mod tests { + use wasabi_leb128::WriteLeb128; + + use super::{Payload, StructuredMemo}; + use crate::memo::Memo; + + #[test] + fn structured_memo() { + let mut bytes = [0; 512]; + bytes[0] = 0xF5; + + // Empty StructuredMemo is rejected + assert!(Memo::from_bytes(&bytes).is_err()); + + bytes[1] = 0x71; + assert_eq!( + Memo::from_bytes(&bytes), + Ok(Memo::Structured(StructuredMemo(vec![Payload::Unknown { + type_id: 0x71, + length: 0, + value: vec![] + }]))) + ); + + bytes[2] = 0x02; + assert_eq!( + Memo::from_bytes(&bytes), + Ok(Memo::Structured(StructuredMemo(vec![Payload::Unknown { + type_id: 0x71, + length: 2, + value: vec![0, 0] + }]))) + ); + + bytes[3] = 0x03; + assert_eq!( + Memo::from_bytes(&bytes), + Ok(Memo::Structured(StructuredMemo(vec![Payload::Unknown { + type_id: 0x71, + length: 2, + value: vec![3, 0] + }]))) + ); + + bytes[4] = 0x04; + assert_eq!( + Memo::from_bytes(&bytes), + Ok(Memo::Structured(StructuredMemo(vec![Payload::Unknown { + type_id: 0x71, + length: 2, + value: vec![3, 4] + }]))) + ); + + bytes[5] = 0x05; + assert_eq!( + Memo::from_bytes(&bytes), + Ok(Memo::Structured(StructuredMemo(vec![ + Payload::Unknown { + type_id: 0x71, + length: 2, + value: vec![3, 4] + }, + Payload::Unknown { + type_id: 5, + length: 0, + value: vec![] + } + ]))) + ); + + let remaining = + 512 - (1 /* 0xF5 */ + 1 /* T1 */ + 1 /* L1 */ + 2 /* V1 */ + 1 /* T2 */ + 2/* L2 */); + (&mut bytes[6..]) + .write_leb128(remaining) + .expect("buffer is large enough"); + assert_eq!( + Memo::from_bytes(&bytes), + Ok(Memo::Structured(StructuredMemo(vec![ + Payload::Unknown { + type_id: 0x71, + length: 2, + value: vec![3, 4] + }, + Payload::Unknown { + type_id: 5, + length: remaining, + value: vec![0; remaining as usize] + } + ]))) + ); + + // Out-of-range length is rejected + (&mut bytes[6..]) + .write_leb128(remaining + 1) + .expect("buffer is large enough"); + assert!(Memo::from_bytes(&bytes).is_err()); + } +} From 31147236a363df761c0715ea3b56cd0bbbc953f6 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sat, 14 Dec 2019 12:14:19 +0000 Subject: [PATCH 2/2] ZIP 302 structured payloads: ReturnAddress, Text --- Cargo.lock | 1 + components/zcash_protocol/Cargo.toml | 2 + components/zcash_protocol/src/memo.rs | 9 ++- components/zcash_protocol/src/memo/builder.rs | 55 ++++++++++++++++ .../zcash_protocol/src/memo/structured.rs | 64 ++++++++++++++++++- 5 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 components/zcash_protocol/src/memo/builder.rs diff --git a/Cargo.lock b/Cargo.lock index 948a656cf7..b4fda20039 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6723,6 +6723,7 @@ dependencies = [ "incrementalmerkletree-testing", "memuse", "proptest", + "sapling-crypto", "wasabi_leb128", ] diff --git a/components/zcash_protocol/Cargo.toml b/components/zcash_protocol/Cargo.toml index 2f8b01eeca..7606cc02e5 100644 --- a/components/zcash_protocol/Cargo.toml +++ b/components/zcash_protocol/Cargo.toml @@ -20,6 +20,8 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] +sapling.workspace = true + # - Logging and metrics memuse = { workspace = true, optional = true } diff --git a/components/zcash_protocol/src/memo.rs b/components/zcash_protocol/src/memo.rs index bf1a8b8c40..05f5d7b3e9 100644 --- a/components/zcash_protocol/src/memo.rs +++ b/components/zcash_protocol/src/memo.rs @@ -11,6 +11,9 @@ use core::str; #[cfg(feature = "std")] use std::error; +mod builder; +pub use builder::Builder; + mod structured; pub use structured::{Payload, StructuredMemo}; @@ -39,6 +42,7 @@ where #[derive(Debug, Clone, PartialEq, Eq)] pub enum Error { InvalidEncoding, + InvalidPayload, InvalidUtf8(core::str::Utf8Error), TooLong(usize), } @@ -47,6 +51,7 @@ impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Error::InvalidEncoding => write!(f, "Invalid memo encoding"), + Error::InvalidPayload => write!(f, "Invalid memo payload"), Error::InvalidUtf8(e) => write!(f, "Invalid UTF-8: {e}"), Error::TooLong(n) => write!(f, "Memo length {n} is larger than maximum of 512"), } @@ -141,7 +146,7 @@ impl MemoBytes { } /// Type-safe wrapper around String to enforce memo length requirements. -#[derive(Clone, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct TextMemo(String); impl From for String { @@ -179,7 +184,7 @@ impl fmt::Debug for Memo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Memo::Empty => write!(f, "Memo::Empty"), - Memo::Text(memo) => write!(f, "Memo::Text(\"{}\")", memo.0), + Memo::Text(memo) => write!(f, "Memo::Text({:?})", memo), Memo::Structured(memo) => write!(f, "Memo::Structured({:?})", memo), Memo::Future(bytes) => write!(f, "Memo::Future({:0x})", bytes.0[0]), Memo::Arbitrary(bytes) => { diff --git a/components/zcash_protocol/src/memo/builder.rs b/components/zcash_protocol/src/memo/builder.rs new file mode 100644 index 0000000000..86b8836431 --- /dev/null +++ b/components/zcash_protocol/src/memo/builder.rs @@ -0,0 +1,55 @@ +//! Memo-building interface. + +use alloc::string::String; +use alloc::vec; +use core::str::FromStr; + +use sapling::PaymentAddress; + +use super::{Error, Memo, Payload, StructuredMemo, TextMemo}; + +pub struct Builder { + return_address: Option, + text: Option, +} + +impl Default for Builder { + fn default() -> Self { + Self::new() + } +} + +impl Builder { + pub fn new() -> Self { + Builder { + return_address: None, + text: None, + } + } + + pub fn return_address(&mut self, return_address: PaymentAddress) -> &mut Self { + self.return_address = Some(return_address); + self + } + + pub fn text(&mut self, text: String) -> &mut Self { + self.text = Some(text); + self + } + + pub fn build(&self) -> Result { + if let Some(pa) = self.return_address.as_ref() { + let mut payloads = vec![Payload::ReturnAddress(pa.clone())]; + + if let Some(s) = self.text.as_ref() { + payloads.push(Payload::Text(TextMemo(s.clone()))); + } + + StructuredMemo::new(payloads).map(Memo::Structured) + } else if let Some(s) = self.text.as_ref() { + Memo::from_str(s) + } else { + Ok(Memo::Empty) + } + } +} diff --git a/components/zcash_protocol/src/memo/structured.rs b/components/zcash_protocol/src/memo/structured.rs index f254f7cabc..3cc767b96c 100644 --- a/components/zcash_protocol/src/memo/structured.rs +++ b/components/zcash_protocol/src/memo/structured.rs @@ -1,14 +1,20 @@ //! Handlers for structured memos. +use alloc::string::String; use alloc::vec::Vec; +use sapling::PaymentAddress; use wasabi_leb128::{ReadLeb128, WriteLeb128}; -use super::Error; +use super::{Error, TextMemo}; /// A payload within a [`StructuredMemo`]. #[derive(Clone, Debug, PartialEq)] pub enum Payload { + /// A Sapling return address. + ReturnAddress(PaymentAddress), + /// UTF-8 text. + Text(TextMemo), /// A payload type we don't know about. Unknown { type_id: u64, @@ -22,6 +28,38 @@ impl Payload { fn parse(mut bytes: &[u8]) -> Result<(&[u8], Option), Error> { match bytes.read_leb128().map_err(|_| Error::InvalidEncoding)? { (0x00, _) => Ok((bytes, None)), + (0x01, _) => { + let (length, _): (u16, _) = + bytes.read_leb128().map_err(|_| Error::InvalidEncoding)?; + if length != 43 || length as usize > bytes.len() { + return Err(Error::InvalidEncoding); + } + + let mut pa_bytes = [0; 43]; + pa_bytes.copy_from_slice(&bytes[..43]); + + PaymentAddress::from_bytes(&pa_bytes) + .ok_or(Error::InvalidEncoding) + .map(|pa| (&bytes[43..], Some(Payload::ReturnAddress(pa)))) + } + (0xa0, _) => { + let (length, _): (u16, _) = + bytes.read_leb128().map_err(|_| Error::InvalidEncoding)?; + if length as usize > bytes.len() { + return Err(Error::InvalidEncoding); + } + + let (value, rem) = bytes.split_at(length as usize); + + // Convert to UTF8, replacing invalid sequences with the replacement + // character U+FFFD + Ok(( + rem, + Some(Payload::Text(TextMemo( + String::from_utf8_lossy(value).into(), + ))), + )) + } (type_id, _) => { let (length, _): (u16, _) = bytes.read_leb128().map_err(|_| Error::InvalidEncoding)?; @@ -47,6 +85,13 @@ impl Payload { fn serialized_len(&self) -> usize { let mut buf = [0; wasabi_leb128::max_bytes::()]; match self { + Payload::ReturnAddress(_) => 45, + Payload::Text(s) => { + let length_len = (&mut buf[..]) + .write_leb128(s.len()) + .expect("buffer is large enough"); + 1 + length_len + s.len() + } Payload::Unknown { type_id, length, .. } => { @@ -67,6 +112,19 @@ impl Payload { /// fit into `buf` by checking `serialized_len`. fn serialize(&self, mut buf: &mut [u8]) -> usize { match self { + Payload::ReturnAddress(pa) => { + let mut written = buf.write_leb128(0x01).expect("buffer is large enough"); + written += buf.write_leb128(43).expect("buffer is large enough"); + assert_eq!(written, 2); + buf[..43].copy_from_slice(&pa.to_bytes()); + 45 + } + Payload::Text(s) => { + let mut written = buf.write_leb128(0xa0).expect("buffer is large enough"); + written += buf.write_leb128(s.len()).expect("buffer is large enough"); + buf[..s.len()].copy_from_slice(s.as_bytes()); + written + s.len() + } Payload::Unknown { type_id, length, @@ -98,9 +156,9 @@ impl StructuredMemo { /// Pack a set of [`Payload`]s into a `StructuredMemo`. /// /// Returns an error if `payloads` is empty or will not fit into a memo field. - pub fn new(payloads: Vec) -> Result { + pub fn new(payloads: Vec) -> Result { if payloads.is_empty() || payloads.iter().map(|p| p.serialized_len()).sum::() > 511 { - Err(()) + Err(Error::InvalidPayload) } else { Ok(StructuredMemo(payloads)) }