zingo_memo/
lib.rs

1//! Zingo-Memo
2//!
3//! Utilities for procedural creation and parsing of the Memo field.
4//!
5//! These memos are not directly exposed to the user,
6//! but instead write down UAs on-chain for recovery after rescan.
7
8#![warn(missing_docs)]
9use std::io::{self, Read, Write};
10
11use zcash_address::unified::{Address, Container, Encoding, Receiver};
12use zcash_client_backend::address::UnifiedAddress;
13use zcash_encoding::{CompactSize, Vector};
14
15/// A parsed memo.
16/// The main use-case for this is to record the UAs that a foreign recipient provided,
17/// as the blockchain only records the pool-specific receiver corresponding to the key we sent with.
18/// We also record the index of any ephemeral addresses sent to. On rescan, this tells us:
19/// * this transaction is the first step of a multistep proposal that is sending
20///     to a TEX address in the second step
21/// * what ephemeral address we need to derive in order to sync the second step
22#[derive(Debug)]
23pub enum ParsedMemo {
24    /// the memo including only a list of unified addresses
25    Version0 {
26        /// The list of unified addresses
27        uas: Vec<UnifiedAddress>,
28    },
29    /// the memo including unified addresses and ephemeral indexes
30    Version1 {
31        /// the list of unified addresses
32        uas: Vec<UnifiedAddress>,
33        /// The ephemeral address indexes
34        rejection_address_indexes: Vec<u32>,
35    },
36}
37
38/// Packs a list of UAs into a memo. The UA only memo is version 0 of the protocol
39/// Note that a UA's raw representation is 1 byte for length, +21 for a T-receiver,
40/// +44 for a Sapling receiver, and +44 for an Orchard receiver. This totals a maximum
41/// of 110 bytes per UA, and attempting to write more than 510 bytes will cause an error.
42#[deprecated(note = "prefer version 1")]
43pub fn create_wallet_internal_memo_version_0(uas: &[UnifiedAddress]) -> io::Result<[u8; 511]> {
44    let mut version_and_data = Vec::new();
45    CompactSize::write(&mut version_and_data, 0usize)?;
46    Vector::write(&mut version_and_data, uas, |w, ua| {
47        write_unified_address_to_raw_encoding(ua, w)
48    })?;
49    let mut uas_bytes = [0u8; 511];
50    if version_and_data.len() > 511 {
51        Err(io::Error::new(
52            io::ErrorKind::InvalidData,
53            "Too many uas to fit in memo field",
54        ))
55    } else {
56        uas_bytes[..version_and_data.len()].copy_from_slice(version_and_data.as_slice());
57        Ok(uas_bytes)
58    }
59}
60
61/// Packs a list of UAs and/or ephemeral address indexes. into a memo.
62/// Note that a UA's raw representation is 1 byte for length, +21 for a T-receiver,
63/// +44 for a Sapling receiver, and +44 for an Orchard receiver. This totals a maximum
64/// of 110 bytes per UA, and attempting to write more than 510 bytes will cause an error.
65/// Ephemeral address indexes are CompactSize encoded, so for most use cases will only be
66/// one byte.
67pub fn create_wallet_internal_memo_version_1(
68    uas: &[UnifiedAddress],
69    refund_address_indexes: &[u32],
70) -> io::Result<[u8; 511]> {
71    let mut memo_bytes_vec = Vec::new();
72    CompactSize::write(&mut memo_bytes_vec, 1usize)?;
73    Vector::write(&mut memo_bytes_vec, uas, |w, ua| {
74        write_unified_address_to_raw_encoding(ua, w)
75    })?;
76    Vector::write(
77        &mut memo_bytes_vec,
78        refund_address_indexes,
79        |w, ea_index| CompactSize::write(w, *ea_index as usize),
80    )?;
81    let mut memo_bytes = [0u8; 511];
82    if memo_bytes_vec.len() > 511 {
83        Err(io::Error::new(
84            io::ErrorKind::InvalidData,
85            "Too many addresses to fit in memo field",
86        ))
87    } else {
88        memo_bytes[..memo_bytes_vec.len()].copy_from_slice(memo_bytes_vec.as_slice());
89        Ok(memo_bytes)
90    }
91}
92
93/// Attempts to parse the 511 bytes of a zingo memo
94pub fn parse_zingo_memo(memo: [u8; 511]) -> io::Result<ParsedMemo> {
95    let mut reader: &[u8] = &memo;
96    match CompactSize::read(&mut reader)? {
97        0 => Ok(ParsedMemo::Version0 {
98            uas: Vector::read(&mut reader, |r| read_unified_address_from_raw_encoding(r))?,
99        }),
100        1 => Ok(ParsedMemo::Version1 {
101            uas: Vector::read(&mut reader, |r| read_unified_address_from_raw_encoding(r))?,
102            rejection_address_indexes: Vector::read(&mut reader, |r| CompactSize::read_t(r))?,
103        }),
104        _ => Err(io::Error::new(
105            io::ErrorKind::InvalidData,
106            "Received memo from a future version of this protocol.\n\
107            Please ensure your software is up-to-date",
108        )),
109    }
110}
111
112/// A helper function to encode a UA as a CompactSize specifying the number
113/// of receivers, followed by the UA's raw encoding as specified in
114/// <https://zips.z.cash/zip-0316#encoding-of-unified-addresses>
115pub fn write_unified_address_to_raw_encoding<W: Write>(
116    ua: &UnifiedAddress,
117    writer: W,
118) -> io::Result<()> {
119    let mainnet_encoded_ua = ua.encode(&zcash_primitives::consensus::MAIN_NETWORK);
120    let (_mainnet, address) =
121        Address::decode(&mainnet_encoded_ua).expect("Freshly encoded ua to decode!");
122    let receivers = address.items();
123    Vector::write(writer, &receivers, |mut w, receiver| {
124        let (typecode, data): (u32, &[u8]) = match receiver {
125            Receiver::Orchard(data) => (3, data),
126            Receiver::Sapling(data) => (2, data),
127            Receiver::P2sh(data) => (1, data),
128            Receiver::P2pkh(data) => (0, data),
129            Receiver::Unknown { typecode, data } => (*typecode, data.as_slice()),
130        };
131        CompactSize::write(&mut w, typecode as usize)?;
132        CompactSize::write(&mut w, data.len())?;
133        w.write_all(data)
134    })
135}
136
137/// A helper function to decode a UA from a CompactSize specifying the number of
138/// receivers, followed by the UA's raw encoding as specified in
139/// <https://zips.z.cash/zip-0316#encoding-of-unified-addresses>
140pub fn read_unified_address_from_raw_encoding<R: Read>(reader: R) -> io::Result<UnifiedAddress> {
141    let receivers = Vector::read(reader, |mut r| {
142        let typecode: usize = CompactSize::read_t(&mut r)?;
143        let addr_len: usize = CompactSize::read_t(&mut r)?;
144        let mut receiver_bytes = vec![0; addr_len];
145        r.read_exact(&mut receiver_bytes)?;
146        decode_receiver(typecode, receiver_bytes)
147    })?;
148    let address = Address::try_from_items(receivers)
149        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
150    UnifiedAddress::try_from(address).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
151}
152
153fn decode_receiver(typecode: usize, data: Vec<u8>) -> io::Result<Receiver> {
154    Ok(match typecode {
155        0 => Receiver::P2pkh(<[u8; 20]>::try_from(data).map_err(|e| {
156            io::Error::new(
157                io::ErrorKind::InvalidData,
158                format!(
159                    "Typecode {typecode} (P2pkh) indicates 20 bytes, found length of {}",
160                    e.len()
161                ),
162            )
163        })?),
164        1 => Receiver::P2sh(<[u8; 20]>::try_from(data).map_err(|e| {
165            io::Error::new(
166                io::ErrorKind::InvalidData,
167                format!(
168                    "Typecode {typecode} (P2sh) indicates 20 bytes, found length of {}",
169                    e.len()
170                ),
171            )
172        })?),
173        2 => Receiver::Sapling(<[u8; 43]>::try_from(data).map_err(|e| {
174            io::Error::new(
175                io::ErrorKind::InvalidData,
176                format!(
177                    "Typecode {typecode} (Sapling) indicates 43 bytes, found length of {}",
178                    e.len()
179                ),
180            )
181        })?),
182        3 => Receiver::Orchard(<[u8; 43]>::try_from(data).map_err(|e| {
183            io::Error::new(
184                io::ErrorKind::InvalidData,
185                format!(
186                    "Typecode {typecode} (Orchard) indicates 43 bytes, found length of {}",
187                    e.len()
188                ),
189            )
190        })?),
191        _ => Receiver::Unknown {
192            typecode: typecode as u32,
193            data,
194        },
195    })
196}
197
198#[cfg(test)]
199mod test_vectors;
200
201#[cfg(test)]
202mod tests {
203    use super::test_vectors as zingomemo_vectors;
204    use super::*;
205    use rand::{self, Rng};
206    use test_vectors::TestVector;
207    use zcash_primitives::consensus::MAIN_NETWORK;
208
209    fn get_some_number_of_ephemeral_indexes() -> Vec<u32> {
210        // Generate a random number of elements between 0 and 10
211        let count = rand::thread_rng().gen_range(0..=10);
212
213        // Create a vector of increasing natural numbers
214        (0..count).collect::<Vec<u32>>()
215    }
216    fn get_serialiazed_ua(test_vector: &TestVector) -> (UnifiedAddress, Vec<u8>) {
217        let zcash_keys::address::Address::Unified(ua) =
218            zcash_keys::address::Address::decode(&MAIN_NETWORK, test_vector.unified_addr).unwrap()
219        else {
220            panic!("Couldn't decode test_vector UA")
221        };
222        let mut serialized_ua = Vec::new();
223        write_unified_address_to_raw_encoding(&ua, &mut serialized_ua).unwrap();
224        (ua, serialized_ua)
225    }
226    #[test]
227    fn parse_zingo_memo_version_n() {
228        for test_vector in zingomemo_vectors::UA_TEST_VECTORS {
229            let (ua, _serialized_ua) = get_serialiazed_ua(test_vector);
230            // version0
231            #[allow(deprecated)]
232            let version0_bytes = create_wallet_internal_memo_version_0(&[ua.clone()]).unwrap();
233            let success_parse = parse_zingo_memo(version0_bytes).expect("To succeed in parse.");
234            if let ParsedMemo::Version0 { uas } = success_parse {
235                assert_eq!(uas[0], ua);
236            };
237            // version1
238            let random_rejection_indexes = get_some_number_of_ephemeral_indexes();
239            let version1_bytes =
240                create_wallet_internal_memo_version_1(&[ua.clone()], &random_rejection_indexes)
241                    .expect("To create version 1 bytes");
242            let success_parse = parse_zingo_memo(version1_bytes).expect("To succeed in parse.");
243            if let ParsedMemo::Version1 {
244                uas,
245                rejection_address_indexes,
246            } = success_parse
247            {
248                assert_eq!(uas[0], ua);
249                assert_eq!(rejection_address_indexes, random_rejection_indexes);
250            };
251        }
252    }
253    #[test]
254    fn round_trip_ser_deser() {
255        for test_vector in zingomemo_vectors::UA_TEST_VECTORS {
256            let (ua, serialized_ua) = get_serialiazed_ua(test_vector);
257            assert_eq!(
258                ua,
259                read_unified_address_from_raw_encoding(&*serialized_ua).unwrap()
260            );
261        }
262    }
263}