Split memo-handling into MemoBytes struct and Memo enum

The MemoBytes struct is a minimal wrapper around the memo bytes, and only
imposes the existence of null-padding for shorter memos. The only error
case is attempting to construct a memo that is too long. MemoBytes is
guaranteed to be round-trip encodable (modulo null padding).

The Memo enum implements the additional memo rules defined in ZIP 302,
interpreting the contents of a memo (for example, parsing it as text).
This commit is contained in:
Jack Grigg 2020-10-29 16:48:26 +00:00
parent 48f7ef84a4
commit c7a3ef0e88
11 changed files with 330 additions and 150 deletions

View File

@ -7,7 +7,7 @@ use std::fmt::Debug;
use zcash_primitives::{
block::BlockHash,
consensus::BlockHeight,
memo::Memo,
memo::{Memo, MemoBytes},
merkle_tree::{CommitmentTree, IncrementalWitness},
primitives::{Nullifier, PaymentAddress},
sapling::Node,
@ -137,11 +137,11 @@ pub trait WalletRead {
anchor_height: BlockHeight,
) -> Result<Amount, Self::Error>;
/// Returns the memo for a note, if it is known and a valid UTF-8 string.
/// Returns the memo for a note.
///
/// This will return `Ok(None)` if the note identifier does not appear in the
/// database as a known note ID.
fn get_memo_as_utf8(&self, id_note: Self::NoteRef) -> Result<Option<String>, Self::Error>;
/// Implementations of this method must return an error if the note identifier
/// does not appear in the backing data store.
fn get_memo(&self, id_note: Self::NoteRef) -> Result<Memo, Self::Error>;
/// Returns the note commitment tree at the specified block height.
fn get_commitment_tree(
@ -199,7 +199,7 @@ pub struct SentTransaction<'a> {
pub account: AccountId,
pub recipient_address: &'a RecipientAddress,
pub value: Amount,
pub memo: Option<Memo>,
pub memo: Option<MemoBytes>,
}
/// This trait encapsulates the write capabilities required to update stored
@ -259,6 +259,7 @@ pub mod testing {
use zcash_primitives::{
block::BlockHash,
consensus::BlockHeight,
memo::Memo,
merkle_tree::{CommitmentTree, IncrementalWitness},
primitives::{Nullifier, PaymentAddress},
sapling::Node,
@ -342,8 +343,8 @@ pub mod testing {
Ok(Amount::zero())
}
fn get_memo_as_utf8(&self, _id_note: Self::NoteRef) -> Result<Option<String>, Self::Error> {
Ok(None)
fn get_memo(&self, _id_note: Self::NoteRef) -> Result<Memo, Self::Error> {
Ok(Memo::Empty)
}
fn get_commitment_tree(

View File

@ -3,7 +3,7 @@ use std::fmt::Debug;
use zcash_primitives::{
consensus::{self, BranchId, NetworkUpgrade},
memo::Memo,
memo::MemoBytes,
prover::TxProver,
transaction::{
builder::Builder,
@ -155,7 +155,7 @@ pub fn create_spend_to_address<E, N, P, D, R>(
extsk: &ExtendedSpendingKey,
to: &RecipientAddress,
value: Amount,
memo: Option<Memo>,
memo: Option<MemoBytes>,
ovk_policy: OvkPolicy,
) -> Result<R, E>
where

View File

@ -2,7 +2,7 @@ use std::collections::HashMap;
use zcash_primitives::{
consensus::{self, BlockHeight},
memo::Memo,
memo::MemoBytes,
note_encryption::{try_sapling_note_decryption, try_sapling_output_recovery},
primitives::{Note, PaymentAddress},
transaction::Transaction,
@ -23,8 +23,8 @@ pub struct DecryptedOutput {
pub account: AccountId,
/// The address the note was sent to.
pub to: PaymentAddress,
/// The memo included with the note.
pub memo: Memo,
/// The memo bytes included with the note.
pub memo: MemoBytes,
/// True if this output was recovered using an [`OutgoingViewingKey`], meaning that
/// this is a logical output of the transaction.
///

View File

@ -275,7 +275,7 @@ mod tests {
use zcash_primitives::{
consensus::{BlockHeight, Network},
constants::SPENDING_KEY_GENERATOR,
memo::Memo,
memo::MemoBytes,
merkle_tree::CommitmentTree,
note_encryption::SaplingNoteEncryption,
primitives::{Note, Nullifier, SaplingIvk},
@ -345,7 +345,7 @@ mod tests {
Some(extfvk.fvk.ovk),
note.clone(),
to,
Memo::default(),
MemoBytes::default(),
&mut rng,
);
let cmu = note.cmu().to_repr().as_ref().to_owned();

View File

@ -36,7 +36,7 @@ pub enum SqliteClientError {
Io(std::io::Error),
/// A received memo cannot be interpreted as a UTF-8 string.
InvalidMemo(std::str::Utf8Error),
InvalidMemo(zcash_primitives::memo::Error),
/// Wrapper for errors from zcash_client_backend
BackendError(data_api::error::Error<NoteId>),
@ -98,6 +98,12 @@ impl From<bs58::decode::Error> for SqliteClientError {
}
}
impl From<zcash_primitives::memo::Error> for SqliteClientError {
fn from(e: zcash_primitives::memo::Error) -> Self {
SqliteClientError::InvalidMemo(e)
}
}
impl From<data_api::error::Error<NoteId>> for SqliteClientError {
fn from(e: data_api::error::Error<NoteId>) -> Self {
SqliteClientError::BackendError(e)

View File

@ -33,6 +33,7 @@ use rusqlite::{Connection, Statement, NO_PARAMS};
use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight},
memo::Memo,
merkle_tree::{CommitmentTree, IncrementalWitness},
primitives::{Nullifier, PaymentAddress},
sapling::Node,
@ -205,10 +206,10 @@ impl<P: consensus::Parameters> WalletRead for WalletDB<P> {
wallet::get_balance_at(self, account, anchor_height)
}
fn get_memo_as_utf8(&self, id_note: Self::NoteRef) -> Result<Option<String>, Self::Error> {
fn get_memo(&self, id_note: Self::NoteRef) -> Result<Memo, Self::Error> {
match id_note {
NoteId::SentNoteId(id_note) => wallet::get_sent_memo_as_utf8(self, id_note),
NoteId::ReceivedNoteId(id_note) => wallet::get_received_memo_as_utf8(self, id_note),
NoteId::SentNoteId(id_note) => wallet::get_sent_memo(self, id_note),
NoteId::ReceivedNoteId(id_note) => wallet::get_received_memo(self, id_note),
}
}
@ -317,8 +318,8 @@ impl<'a, P: consensus::Parameters> WalletRead for DataConnStmtCache<'a, P> {
self.wallet_db.get_balance_at(account, anchor_height)
}
fn get_memo_as_utf8(&self, id_note: Self::NoteRef) -> Result<Option<String>, Self::Error> {
self.wallet_db.get_memo_as_utf8(id_note)
fn get_memo(&self, id_note: Self::NoteRef) -> Result<Memo, Self::Error> {
self.wallet_db.get_memo(id_note)
}
fn get_commitment_tree(
@ -545,7 +546,7 @@ mod tests {
use zcash_primitives::{
block::BlockHash,
consensus::{BlockHeight, Network, NetworkUpgrade, Parameters},
memo::Memo,
memo::MemoBytes,
note_encryption::SaplingNoteEncryption,
primitives::{Note, Nullifier, PaymentAddress},
transaction::components::Amount,
@ -602,7 +603,7 @@ mod tests {
Some(extfvk.fvk.ovk),
note.clone(),
to,
Memo::default(),
MemoBytes::default(),
&mut rng,
);
let cmu = note.cmu().to_repr().as_ref().to_vec();
@ -662,7 +663,7 @@ mod tests {
Some(extfvk.fvk.ovk),
note.clone(),
to,
Memo::default(),
MemoBytes::default(),
&mut rng,
);
let cmu = note.cmu().to_repr().as_ref().to_vec();
@ -690,7 +691,7 @@ mod tests {
Some(extfvk.fvk.ovk),
note.clone(),
change_addr,
Memo::default(),
MemoBytes::default(),
&mut rng,
);
let cmu = note.cmu().to_repr().as_ref().to_vec();

View File

@ -3,11 +3,12 @@
use ff::PrimeField;
use rusqlite::{params, OptionalExtension, ToSql, NO_PARAMS};
use std::collections::HashMap;
use std::convert::TryFrom;
use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight, NetworkUpgrade},
memo::Memo,
memo::{Memo, MemoBytes},
merkle_tree::{CommitmentTree, IncrementalWitness},
primitives::{Note, Nullifier, PaymentAddress},
sapling::Node,
@ -41,7 +42,7 @@ pub trait ShieldedOutput {
fn account(&self) -> AccountId;
fn to(&self) -> &PaymentAddress;
fn note(&self) -> &Note;
fn memo(&self) -> Option<&Memo>;
fn memo(&self) -> Option<&MemoBytes>;
fn is_change(&self) -> Option<bool>;
fn nullifier(&self) -> Option<Nullifier>;
}
@ -59,7 +60,7 @@ impl ShieldedOutput for WalletShieldedOutput<Nullifier> {
fn note(&self) -> &Note {
&self.note
}
fn memo(&self) -> Option<&Memo> {
fn memo(&self) -> Option<&MemoBytes> {
None
}
fn is_change(&self) -> Option<bool> {
@ -84,7 +85,7 @@ impl ShieldedOutput for DecryptedOutput {
fn note(&self) -> &Note {
&self.note
}
fn memo(&self) -> Option<&Memo> {
fn memo(&self) -> Option<&MemoBytes> {
Some(&self.memo)
}
fn is_change(&self) -> Option<bool> {
@ -273,32 +274,24 @@ pub fn get_balance_at<P>(
/// use zcash_client_sqlite::{
/// NoteId,
/// WalletDB,
/// wallet::get_received_memo_as_utf8,
/// wallet::get_received_memo,
/// };
///
/// let data_file = NamedTempFile::new().unwrap();
/// let db = WalletDB::for_path(data_file, Network::TestNetwork).unwrap();
/// let memo = get_received_memo_as_utf8(&db, 27);
/// let memo = get_received_memo(&db, 27);
/// ```
pub fn get_received_memo_as_utf8<P>(
wdb: &WalletDB<P>,
id_note: i64,
) -> Result<Option<String>, SqliteClientError> {
let memo: Vec<_> = wdb.conn.query_row(
pub fn get_received_memo<P>(wdb: &WalletDB<P>, id_note: i64) -> Result<Memo, SqliteClientError> {
let memo_bytes: Vec<_> = wdb.conn.query_row(
"SELECT memo FROM received_notes
WHERE id_note = ?",
&[id_note],
|row| row.get(0),
)?;
match Memo::from_bytes(&memo) {
Some(memo) => match memo.to_utf8() {
Some(Ok(res)) => Ok(Some(res)),
Some(Err(e)) => Err(SqliteClientError::InvalidMemo(e)),
None => Ok(None),
},
None => Ok(None),
}
MemoBytes::from_bytes(&memo_bytes)
.and_then(Memo::try_from)
.map_err(SqliteClientError::from)
}
/// Returns the memo for a sent note, if it is known and a valid UTF-8 string.
@ -314,32 +307,24 @@ pub fn get_received_memo_as_utf8<P>(
/// use zcash_client_sqlite::{
/// NoteId,
/// WalletDB,
/// wallet::get_sent_memo_as_utf8,
/// wallet::get_sent_memo,
/// };
///
/// let data_file = NamedTempFile::new().unwrap();
/// let db = WalletDB::for_path(data_file, Network::TestNetwork).unwrap();
/// let memo = get_sent_memo_as_utf8(&db, 12);
/// let memo = get_sent_memo(&db, 12);
/// ```
pub fn get_sent_memo_as_utf8<P>(
wdb: &WalletDB<P>,
id_note: i64,
) -> Result<Option<String>, SqliteClientError> {
let memo: Vec<_> = wdb.conn.query_row(
pub fn get_sent_memo<P>(wdb: &WalletDB<P>, id_note: i64) -> Result<Memo, SqliteClientError> {
let memo_bytes: Vec<_> = wdb.conn.query_row(
"SELECT memo FROM sent_notes
WHERE id_note = ?",
&[id_note],
|row| row.get(0),
)?;
match Memo::from_bytes(&memo) {
Some(memo) => match memo.to_utf8() {
Some(Ok(res)) => Ok(Some(res)),
Some(Err(e)) => Err(SqliteClientError::InvalidMemo(e)),
None => Ok(None),
},
None => Ok(None),
}
MemoBytes::from_bytes(&memo_bytes)
.and_then(Memo::try_from)
.map_err(SqliteClientError::from)
}
pub fn block_height_extrema<P>(
@ -605,7 +590,7 @@ pub fn put_received_note<'a, P, T: ShieldedOutput>(
let diversifier = output.to().diversifier().0.to_vec();
let value = output.note().value as i64;
let rcm = rcm.as_ref();
let memo = output.memo().map(|m| m.as_bytes());
let memo = output.memo().map(|m| m.as_slice());
let is_change = output.is_change();
let tx = tx_ref;
let output_index = output.index() as i64;
@ -694,7 +679,7 @@ pub fn put_sent_note<'a, P: consensus::Parameters>(
account,
to_str,
value,
&output.memo.as_bytes(),
&output.memo.as_slice(),
tx_ref,
output_index
])? == 0
@ -722,7 +707,7 @@ pub fn insert_sent_note<'a, P: consensus::Parameters>(
account: AccountId,
to: &RecipientAddress,
value: Amount,
memo: &Option<Memo>,
memo: &Option<MemoBytes>,
) -> Result<(), SqliteClientError> {
let to_str = to.encode(&stmts.wallet_db.params);
let ivalue: i64 = value.into();
@ -732,7 +717,7 @@ pub fn insert_sent_note<'a, P: consensus::Parameters>(
account.0,
to_str,
ivalue,
memo.as_ref().map(|m| m.as_bytes().to_vec()),
memo.as_ref().map(|m| m.as_slice().to_vec()),
])?;
Ok(())

View File

@ -3,7 +3,7 @@ use ff::Field;
use rand_core::OsRng;
use zcash_primitives::{
consensus::{NetworkUpgrade::Canopy, Parameters, TEST_NETWORK},
memo::Memo,
memo::MemoBytes,
note_encryption::{try_sapling_note_decryption, SaplingNoteEncryption},
primitives::{Diversifier, PaymentAddress, SaplingIvk, ValueCommitment},
transaction::components::{OutputDescription, GROTH_PROOF_SIZE},
@ -36,7 +36,7 @@ fn bench_note_decryption(c: &mut Criterion) {
let note = pa.create_note(value, rseed).unwrap();
let cmu = note.cmu();
let mut ne = SaplingNoteEncryption::new(None, note, pa, Memo::default(), &mut rng);
let mut ne = SaplingNoteEncryption::new(None, note, pa, MemoBytes::default(), &mut rng);
let ephemeral_key = ne.epk().clone().into();
let enc_ciphertext = ne.encrypt_note_plaintext();
let out_ciphertext = ne.encrypt_outgoing_plaintext(&cv, &cmu);

View File

@ -1,6 +1,10 @@
//! Structs for handling encrypted memos.
use std::cmp::Ordering;
use std::convert::{TryFrom, TryInto};
use std::error;
use std::fmt;
use std::ops::Deref;
use std::str;
/// Format a byte array as a colon-delimited hex string.
@ -24,96 +28,259 @@ where
Ok(())
}
/// Errors that may result from attempting to construct an invalid memo.
#[derive(Debug, PartialEq)]
pub enum Error {
InvalidUtf8(std::str::Utf8Error),
TooLong(usize),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::InvalidUtf8(e) => write!(f, "Invalid UTF-8: {}", e),
Error::TooLong(n) => write!(f, "Memo length {} is larger than maximum of 512", n),
}
}
}
impl error::Error for Error {}
/// The unencrypted memo bytes received alongside a shielded note in a Zcash transaction.
#[derive(Clone)]
pub struct MemoBytes(pub(crate) Box<[u8; 512]>);
impl fmt::Debug for MemoBytes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "MemoBytes(")?;
fmt_colon_delimited_hex(f, &self.0[..])?;
write!(f, ")")
}
}
impl Default for MemoBytes {
fn default() -> Self {
let mut bytes = [0u8; 512];
bytes[0] = 0xF6;
MemoBytes(Box::new(bytes))
}
}
impl PartialEq for MemoBytes {
fn eq(&self, rhs: &MemoBytes) -> bool {
self.0[..] == rhs.0[..]
}
}
impl Eq for MemoBytes {}
impl PartialOrd for MemoBytes {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for MemoBytes {
fn cmp(&self, rhs: &Self) -> Ordering {
self.0[..].cmp(&rhs.0[..])
}
}
impl MemoBytes {
/// Creates a `MemoBytes` from a slice.
///
/// Returns an error if the provided slice is longer than 512 bytes. Slices shorter
/// than 512 bytes are padded with null bytes.
pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() > 512 {
return Err(Error::TooLong(bytes.len()));
}
let mut memo = [0u8; 512];
memo[..bytes.len()].copy_from_slice(bytes);
Ok(MemoBytes(Box::new(memo)))
}
/// Returns the raw byte array containing the memo bytes, including null padding.
pub fn as_array(&self) -> &[u8; 512] {
&self.0
}
/// Returns a slice of the raw bytes, excluding null padding.
pub fn as_slice(&self) -> &[u8] {
let first_null = self
.0
.iter()
.enumerate()
.rev()
.find(|(_, &b)| b != 0)
.map(|(i, _)| i + 1)
.unwrap_or_default();
&self.0[..first_null]
}
}
/// Type-safe wrapper around String to enforce memo length requirements.
#[derive(Clone, PartialEq)]
pub struct TextMemo(String);
impl From<TextMemo> for String {
fn from(memo: TextMemo) -> String {
memo.0
}
}
impl Deref for TextMemo {
type Target = str;
#[inline]
fn deref(&self) -> &str {
self.0.deref()
}
}
/// An unencrypted memo received alongside a shielded note in a Zcash transaction.
#[derive(Clone)]
pub struct Memo(pub(crate) [u8; 512]);
pub enum Memo {
/// An empty memo field.
Empty,
/// A memo field containing a UTF-8 string.
Text(TextMemo),
/// Some unknown memo format from ✨*the future*✨ that we can't parse.
Future(MemoBytes),
/// A memo field containing arbitrary bytes.
Arbitrary(Box<[u8; 511]>),
}
impl fmt::Debug for Memo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Memo(")?;
match self.to_utf8() {
Some(Ok(memo)) => write!(f, "\"{}\"", memo)?,
_ => fmt_colon_delimited_hex(f, &self.0[..])?,
match self {
Memo::Empty => write!(f, "Memo::Empty"),
Memo::Text(memo) => write!(f, "Memo::Text(\"{}\")", memo.0),
Memo::Future(bytes) => write!(f, "Memo::Future({:0x})", bytes.0[0]),
Memo::Arbitrary(bytes) => {
write!(f, "Memo::Arbitrary(")?;
fmt_colon_delimited_hex(f, &bytes[..])?;
write!(f, ")")
}
}
write!(f, ")")
}
}
impl Default for Memo {
fn default() -> Self {
// Empty memo field indication per ZIP 302
let mut memo = [0u8; 512];
memo[0] = 0xF6;
Memo(memo)
Memo::Empty
}
}
impl PartialEq for Memo {
fn eq(&self, rhs: &Memo) -> bool {
self.0[..] == rhs.0[..]
match (self, rhs) {
(Memo::Empty, Memo::Empty) => true,
(Memo::Text(a), Memo::Text(b)) => a == b,
(Memo::Future(a), Memo::Future(b)) => a.0[..] == b.0[..],
(Memo::Arbitrary(a), Memo::Arbitrary(b)) => a[..] == b[..],
_ => false,
}
}
}
impl TryFrom<MemoBytes> for Memo {
type Error = Error;
/// Parses a `Memo` from its ZIP 302 serialization.
///
/// Returns an error if the provided slice does not represent a valid `Memo` (for
/// example, if the slice is not 512 bytes, or the encoded `Memo` is non-canonical).
fn try_from(bytes: MemoBytes) -> Result<Self, Self::Error> {
match bytes.0[0] {
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())
.map(|r| Memo::Text(TextMemo(r.to_owned())))
.map_err(Error::InvalidUtf8),
_ => Ok(Memo::Future(bytes)),
}
}
}
impl From<Memo> for MemoBytes {
/// Serializes the `Memo` per ZIP 302.
fn from(memo: Memo) -> Self {
match memo {
// Small optimisation to avoid a clone
Memo::Future(memo) => memo,
memo => (&memo).into(),
}
}
}
impl From<&Memo> for MemoBytes {
/// Serializes the `Memo` per ZIP 302.
fn from(memo: &Memo) -> Self {
match memo {
Memo::Empty => MemoBytes::default(),
Memo::Text(s) => {
let mut bytes = [0u8; 512];
let s_bytes = s.0.as_bytes();
// s_bytes.len() is guaranteed to be <= 512
bytes[..s_bytes.len()].copy_from_slice(s_bytes);
MemoBytes(Box::new(bytes))
}
Memo::Future(memo) => memo.clone(),
Memo::Arbitrary(arb) => {
let mut bytes = [0u8; 512];
bytes[0] = 0xFF;
bytes[1..].copy_from_slice(arb.as_ref());
MemoBytes(Box::new(bytes))
}
}
}
}
impl Memo {
/// Returns a `Memo` containing the given slice, appending with zero bytes if
/// necessary, or `None` if the slice is too long. If the slice is empty,
/// `Memo::default` is returned.
pub fn from_bytes(memo: &[u8]) -> Option<Memo> {
if memo.is_empty() {
Some(Memo::default())
} else if memo.len() <= 512 {
let mut data = [0; 512];
data[0..memo.len()].copy_from_slice(memo);
Some(Memo(data))
} else {
// memo is too long
None
}
/// Parses a `Memo` from its ZIP 302 serialization.
///
/// Returns an error if the provided slice does not represent a valid `Memo` (for
/// example, if the slice is not 512 bytes, or the encoded `Memo` is non-canonical).
pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
MemoBytes::from_bytes(bytes).and_then(TryFrom::try_from)
}
/// Returns the underlying bytes of the `Memo`.
pub fn as_bytes(&self) -> &[u8] {
&self.0[..]
}
/// Returns:
/// - `None` if the memo is not text
/// - `Some(Ok(memo))` if the memo contains a valid UTF-8 string
/// - `Some(Err(e))` if the memo contains invalid UTF-8
pub fn to_utf8(&self) -> Option<Result<String, str::Utf8Error>> {
// Check if it is a text or binary memo
if self.0[0] < 0xF5 {
// Check if it is valid UTF8
Some(str::from_utf8(&self.0).map(|memo| {
// Drop trailing zeroes
memo.trim_end_matches(char::from(0)).to_owned()
}))
} else {
None
}
/// Serializes the `Memo` per ZIP 302.
pub fn encode(&self) -> MemoBytes {
self.into()
}
}
impl str::FromStr for Memo {
type Err = ();
type Err = Error;
/// Returns a `Memo` containing the given string, or an error if the string is too long.
fn from_str(memo: &str) -> Result<Self, Self::Err> {
Memo::from_bytes(memo.as_bytes()).ok_or(())
if memo.is_empty() {
Ok(Memo::Empty)
} else if memo.len() <= 512 {
Ok(Memo::Text(TextMemo(memo.to_owned())))
} else {
Err(Error::TooLong(memo.len()))
}
}
}
#[cfg(test)]
mod tests {
use std::convert::TryInto;
use std::str::FromStr;
use super::Memo;
use super::{Error, Memo, MemoBytes};
#[test]
fn memo_from_str() {
assert_eq!(
Memo::from_str("").unwrap(),
Memo([
Memo::from_str("").unwrap().encode(),
MemoBytes(Box::new([
0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@ -151,7 +318,7 @@ mod tests {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
])
]))
);
assert_eq!(
Memo::from_str(
@ -163,8 +330,9 @@ mod tests {
meeeeeeeeeeeeeeeeeeemooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo \
but it's just short enough"
)
.unwrap(),
Memo([
.unwrap()
.encode(),
MemoBytes(Box::new([
0x74, 0x68, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69,
0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69,
0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69,
@ -202,7 +370,7 @@ mod tests {
0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x20, 0x62, 0x75, 0x74, 0x20,
0x69, 0x74, 0x27, 0x73, 0x20, 0x6a, 0x75, 0x73, 0x74, 0x20, 0x73, 0x68, 0x6f, 0x72,
0x74, 0x20, 0x65, 0x6e, 0x6f, 0x75, 0x67, 0x68
])
]))
);
assert_eq!(
Memo::from_str(
@ -214,14 +382,27 @@ mod tests {
meeeeeeeeeeeeeeeeeeemooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo \
but it's now a bit too long"
),
Err(())
Err(Error::TooLong(513))
);
}
#[test]
fn memo_to_utf8() {
let memo = Memo::from_str("Test memo").unwrap();
assert_eq!(memo.to_utf8(), Some(Ok("Test memo".to_owned())));
assert_eq!(Memo::default().to_utf8(), None);
fn future_memo() {
let bytes = [0xFE; 512];
assert_eq!(
MemoBytes::from_bytes(&bytes).unwrap().try_into(),
Ok(Memo::Future(MemoBytes(Box::new(bytes))))
);
}
#[test]
fn arbitrary_memo() {
let bytes = [42; 511];
let memo = Memo::Arbitrary(Box::new(bytes));
let raw = memo.encode();
let encoded = raw.as_array();
assert_eq!(encoded[0], 0xFF);
assert_eq!(encoded[1..], bytes[..]);
assert_eq!(MemoBytes::from_bytes(encoded).unwrap().try_into(), Ok(memo));
}
}

View File

@ -2,7 +2,7 @@
use crate::{
consensus::{self, BlockHeight, NetworkUpgrade::Canopy, ZIP212_GRACE_PERIOD},
memo::Memo,
memo::MemoBytes,
primitives::{Diversifier, Note, PaymentAddress, Rseed, SaplingIvk},
};
use blake2b_simd::{Hash as Blake2bHash, Params as Blake2bParams};
@ -113,7 +113,7 @@ pub fn prf_ock(
/// use rand_core::OsRng;
/// use zcash_primitives::{
/// keys::{OutgoingViewingKey, prf_expand},
/// memo::Memo,
/// memo::MemoBytes,
/// note_encryption::SaplingNoteEncryption,
/// primitives::{Diversifier, PaymentAddress, Rseed, ValueCommitment},
/// };
@ -135,7 +135,7 @@ pub fn prf_ock(
/// let note = to.create_note(value, Rseed::BeforeZip212(rcm)).unwrap();
/// let cmu = note.cmu();
///
/// let mut enc = SaplingNoteEncryption::new(ovk, note, to, Memo::default(), &mut rng);
/// let mut enc = SaplingNoteEncryption::new(ovk, note, to, MemoBytes::default(), &mut rng);
/// let encCiphertext = enc.encrypt_note_plaintext();
/// let outCiphertext = enc.encrypt_outgoing_plaintext(&cv.commitment().into(), &cmu);
/// ```
@ -144,7 +144,7 @@ pub struct SaplingNoteEncryption<R: RngCore> {
esk: jubjub::Fr,
note: Note,
to: PaymentAddress,
memo: Memo,
memo: MemoBytes,
/// `None` represents the `ovk = ⊥` case.
ovk: Option<OutgoingViewingKey>,
rng: R,
@ -159,7 +159,7 @@ impl<R: RngCore + CryptoRng> SaplingNoteEncryption<R> {
ovk: Option<OutgoingViewingKey>,
note: Note,
to: PaymentAddress,
memo: Memo,
memo: MemoBytes,
rng: R,
) -> Self {
Self::new_internal(ovk, note, to, memo, rng)
@ -171,7 +171,7 @@ impl<R: RngCore> SaplingNoteEncryption<R> {
ovk: Option<OutgoingViewingKey>,
note: Note,
to: PaymentAddress,
memo: Memo,
memo: MemoBytes,
mut rng: R,
) -> Self {
let esk = note.generate_or_derive_esk_internal(&mut rng);
@ -222,7 +222,7 @@ impl<R: RngCore> SaplingNoteEncryption<R> {
input[20..COMPACT_NOTE_SIZE].copy_from_slice(&rseed);
}
}
input[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE].copy_from_slice(&self.memo.0);
input[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE].copy_from_slice(self.memo.as_array());
let mut output = [0u8; ENC_CIPHERTEXT_SIZE];
assert_eq!(
@ -362,7 +362,7 @@ pub fn try_sapling_note_decryption<P: consensus::Parameters>(
epk: &jubjub::ExtendedPoint,
cmu: &bls12_381::Scalar,
enc_ciphertext: &[u8],
) -> Option<(Note, PaymentAddress, Memo)> {
) -> Option<(Note, PaymentAddress, MemoBytes)> {
assert_eq!(enc_ciphertext.len(), ENC_CIPHERTEXT_SIZE);
let shared_secret = sapling_ka_agree(&ivk.0, &epk);
@ -384,10 +384,10 @@ pub fn try_sapling_note_decryption<P: consensus::Parameters>(
let (note, to) = parse_note_plaintext_without_memo(params, height, ivk, epk, cmu, &plaintext)?;
let mut memo = [0u8; 512];
memo.copy_from_slice(&plaintext[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE]);
// Memo is the correct length by definition.
let memo = MemoBytes::from_bytes(&plaintext[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE]).unwrap();
Some((note, to, Memo(memo)))
Some((note, to, memo))
}
/// Trial decryption of the compact note plaintext by the recipient for light clients.
@ -436,7 +436,7 @@ pub fn try_sapling_output_recovery_with_ock<P: consensus::Parameters>(
epk: &jubjub::ExtendedPoint,
enc_ciphertext: &[u8],
out_ciphertext: &[u8],
) -> Option<(Note, PaymentAddress, Memo)> {
) -> Option<(Note, PaymentAddress, MemoBytes)> {
assert_eq!(enc_ciphertext.len(), ENC_CIPHERTEXT_SIZE);
assert_eq!(out_ciphertext.len(), OUT_CIPHERTEXT_SIZE);
@ -502,8 +502,7 @@ pub fn try_sapling_output_recovery_with_ock<P: consensus::Parameters>(
Rseed::AfterZip212(r)
};
let mut memo = [0u8; 512];
memo.copy_from_slice(&plaintext[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE]);
let memo = MemoBytes::from_bytes(&plaintext[COMPACT_NOTE_SIZE..NOTE_PLAINTEXT_SIZE]).unwrap();
let diversifier = Diversifier(d);
if (diversifier.g_d()? * esk).to_bytes() != epk.to_bytes() {
@ -525,7 +524,7 @@ pub fn try_sapling_output_recovery_with_ock<P: consensus::Parameters>(
}
}
Some((note, to, Memo(memo)))
Some((note, to, memo))
}
/// Recovery of the full note plaintext by the sender.
@ -545,7 +544,7 @@ pub fn try_sapling_output_recovery<P: consensus::Parameters>(
epk: &jubjub::ExtendedPoint,
enc_ciphertext: &[u8],
out_ciphertext: &[u8],
) -> Option<(Note, PaymentAddress, Memo)> {
) -> Option<(Note, PaymentAddress, MemoBytes)> {
try_sapling_output_recovery_with_ock::<P>(
params,
height,
@ -582,7 +581,7 @@ mod tests {
Parameters, TEST_NETWORK, ZIP212_GRACE_PERIOD,
},
keys::OutgoingViewingKey,
memo::Memo,
memo::MemoBytes,
primitives::{Diversifier, PaymentAddress, Rseed, SaplingIvk, ValueCommitment},
util::generate_random_rseed,
};
@ -682,7 +681,8 @@ mod tests {
let cmu = note.cmu();
let ovk = OutgoingViewingKey([0; 32]);
let mut ne = SaplingNoteEncryption::new(Some(ovk), note, pa, Memo([0; 512]), &mut rng);
let mut ne =
SaplingNoteEncryption::new(Some(ovk), note, pa, MemoBytes::default(), &mut rng);
let epk = ne.epk().clone().into();
let enc_ciphertext = ne.encrypt_note_plaintext();
let out_ciphertext = ne.encrypt_outgoing_plaintext(&cv, &cmu);
@ -1671,7 +1671,7 @@ mod tests {
Some((decrypted_note, decrypted_to, decrypted_memo)) => {
assert_eq!(decrypted_note, note);
assert_eq!(decrypted_to, to);
assert_eq!(&decrypted_memo.0[..], &tv.memo[..]);
assert_eq!(&decrypted_memo.as_array()[..], &tv.memo[..]);
}
None => panic!("Note decryption failed"),
}
@ -1704,7 +1704,7 @@ mod tests {
Some((decrypted_note, decrypted_to, decrypted_memo)) => {
assert_eq!(decrypted_note, note);
assert_eq!(decrypted_to, to);
assert_eq!(&decrypted_memo.0[..], &tv.memo[..]);
assert_eq!(&decrypted_memo.as_array()[..], &tv.memo[..]);
}
None => panic!("Output recovery failed"),
}
@ -1713,7 +1713,13 @@ mod tests {
// Test encryption
//
let mut ne = SaplingNoteEncryption::new(Some(ovk), note, to, Memo(tv.memo), OsRng);
let mut ne = SaplingNoteEncryption::new(
Some(ovk),
note,
to,
MemoBytes::from_bytes(&tv.memo).unwrap(),
OsRng,
);
// Swap in the ephemeral keypair from the test vectors
ne.esk = esk;
ne.epk = epk.into_subgroup().unwrap();

View File

@ -14,7 +14,7 @@ use crate::{
consensus::{self, BlockHeight},
keys::OutgoingViewingKey,
legacy::TransparentAddress,
memo::Memo,
memo::MemoBytes,
merkle_tree::MerklePath,
note_encryption::SaplingNoteEncryption,
primitives::{Diversifier, Note, PaymentAddress},
@ -99,7 +99,7 @@ pub struct SaplingOutput {
ovk: Option<OutgoingViewingKey>,
to: PaymentAddress,
note: Note,
memo: Memo,
memo: MemoBytes,
}
impl SaplingOutput {
@ -110,7 +110,7 @@ impl SaplingOutput {
ovk: Option<OutgoingViewingKey>,
to: PaymentAddress,
value: Amount,
memo: Option<Memo>,
memo: Option<MemoBytes>,
) -> Result<Self, Error> {
Self::new_internal(params, height, rng, ovk, to, value, memo)
}
@ -122,7 +122,7 @@ impl SaplingOutput {
ovk: Option<OutgoingViewingKey>,
to: PaymentAddress,
value: Amount,
memo: Option<Memo>,
memo: Option<MemoBytes>,
) -> Result<Self, Error> {
let g_d = to.g_d().ok_or(Error::InvalidAddress)?;
if value.is_negative() {
@ -521,7 +521,7 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
ovk: Option<OutgoingViewingKey>,
to: PaymentAddress,
value: Amount,
memo: Option<Memo>,
memo: Option<MemoBytes>,
) -> Result<(), Error> {
let output = SaplingOutput::new_internal(
&self.params,