Merge pull request #284 from str4d/259-ovk-none

zcash_primitives: Support ovk = ⊥ in note encryption
This commit is contained in:
ebfull 2020-09-09 14:32:57 -06:00 committed by GitHub
commit c7e9523dce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 105 additions and 81 deletions

View File

@ -257,7 +257,7 @@ mod tests {
rseed, rseed,
}; };
let encryptor = SaplingNoteEncryption::new( let encryptor = SaplingNoteEncryption::new(
extfvk.fvk.ovk, Some(extfvk.fvk.ovk),
note.clone(), note.clone(),
to.clone(), to.clone(),
Memo::default(), Memo::default(),

View File

@ -133,7 +133,7 @@ mod tests {
rseed, rseed,
}; };
let encryptor = SaplingNoteEncryption::new( let encryptor = SaplingNoteEncryption::new(
extfvk.fvk.ovk, Some(extfvk.fvk.ovk),
note.clone(), note.clone(),
to.clone(), to.clone(),
Memo::default(), Memo::default(),
@ -193,7 +193,7 @@ mod tests {
rseed, rseed,
}; };
let encryptor = SaplingNoteEncryption::new( let encryptor = SaplingNoteEncryption::new(
extfvk.fvk.ovk, Some(extfvk.fvk.ovk),
note.clone(), note.clone(),
to, to,
Memo::default(), Memo::default(),
@ -221,7 +221,7 @@ mod tests {
rseed, rseed,
}; };
let encryptor = SaplingNoteEncryption::new( let encryptor = SaplingNoteEncryption::new(
extfvk.fvk.ovk, Some(extfvk.fvk.ovk),
note.clone(), note.clone(),
change_addr, change_addr,
Memo::default(), Memo::default(),

View File

@ -1,7 +1,7 @@
//! Functions for creating transactions. //! Functions for creating transactions.
use ff::PrimeField; use ff::PrimeField;
use rand_core::{OsRng, RngCore}; use rand_core::OsRng;
use rusqlite::{types::ToSql, Connection, NO_PARAMS}; use rusqlite::{types::ToSql, Connection, NO_PARAMS};
use std::convert::TryInto; use std::convert::TryInto;
use std::path::Path; use std::path::Path;
@ -148,16 +148,9 @@ pub fn create_to_address<P: AsRef<Path>>(
// Apply the outgoing viewing key policy. // Apply the outgoing viewing key policy.
let ovk = match ovk_policy { let ovk = match ovk_policy {
OvkPolicy::Sender => extfvk.fvk.ovk, OvkPolicy::Sender => Some(extfvk.fvk.ovk),
OvkPolicy::Custom(ovk) => ovk, OvkPolicy::Custom(ovk) => Some(ovk),
OvkPolicy::Discard => { OvkPolicy::Discard => None,
// Generate a random outgoing viewing key that the caller does not know.
// The probability of this colliding with a legitimate outgoing viewing
// key is negligible.
let mut ovk = [0; 32];
OsRng.fill_bytes(&mut ovk);
OutgoingViewingKey(ovk)
}
}; };
// Target the next block, assuming we are up-to-date. // Target the next block, assuming we are up-to-date.

View File

@ -152,6 +152,21 @@ fn kdf_sapling(dhsecret: jubjub::SubgroupPoint, epk: &jubjub::SubgroupPoint) ->
.finalize() .finalize()
} }
/// A symmetric key that can be used to recover a single Sapling output.
pub struct OutgoingCipherKey([u8; 32]);
impl From<[u8; 32]> for OutgoingCipherKey {
fn from(ock: [u8; 32]) -> Self {
OutgoingCipherKey(ock)
}
}
impl AsRef<[u8]> for OutgoingCipherKey {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
/// Sapling PRF^ock. /// Sapling PRF^ock.
/// ///
/// Implemented per section 5.4.2 of the Zcash Protocol Specification. /// Implemented per section 5.4.2 of the Zcash Protocol Specification.
@ -160,16 +175,21 @@ pub fn prf_ock(
cv: &jubjub::ExtendedPoint, cv: &jubjub::ExtendedPoint,
cmu: &bls12_381::Scalar, cmu: &bls12_381::Scalar,
epk: &jubjub::SubgroupPoint, epk: &jubjub::SubgroupPoint,
) -> Blake2bHash { ) -> OutgoingCipherKey {
Blake2bParams::new() OutgoingCipherKey(
.hash_length(32) Blake2bParams::new()
.personal(PRF_OCK_PERSONALIZATION) .hash_length(32)
.to_state() .personal(PRF_OCK_PERSONALIZATION)
.update(&ovk.0) .to_state()
.update(&cv.to_bytes()) .update(&ovk.0)
.update(&cmu.to_repr()) .update(&cv.to_bytes())
.update(&epk.to_bytes()) .update(&cmu.to_repr())
.finalize() .update(&epk.to_bytes())
.finalize()
.as_bytes()
.try_into()
.unwrap(),
)
} }
/// An API for encrypting Sapling notes. /// An API for encrypting Sapling notes.
@ -201,7 +221,7 @@ pub fn prf_ock(
/// let diversifier = Diversifier([0; 11]); /// let diversifier = Diversifier([0; 11]);
/// let pk_d = diversifier.g_d().unwrap(); /// let pk_d = diversifier.g_d().unwrap();
/// let to = PaymentAddress::from_parts(diversifier, pk_d).unwrap(); /// let to = PaymentAddress::from_parts(diversifier, pk_d).unwrap();
/// let ovk = OutgoingViewingKey([0; 32]); /// let ovk = Some(OutgoingViewingKey([0; 32]));
/// ///
/// let value = 1000; /// let value = 1000;
/// let rcv = jubjub::Fr::random(&mut rng); /// let rcv = jubjub::Fr::random(&mut rng);
@ -213,29 +233,34 @@ pub fn prf_ock(
/// let note = to.create_note(value, Rseed::BeforeZip212(rcm)).unwrap(); /// let note = to.create_note(value, Rseed::BeforeZip212(rcm)).unwrap();
/// let cmu = note.cmu(); /// let cmu = note.cmu();
/// ///
/// let enc = SaplingNoteEncryption::new(ovk, note, to, Memo::default(), &mut rng); /// let mut enc = SaplingNoteEncryption::new(ovk, note, to, Memo::default(), &mut rng);
/// let encCiphertext = enc.encrypt_note_plaintext(); /// let encCiphertext = enc.encrypt_note_plaintext();
/// let outCiphertext = enc.encrypt_outgoing_plaintext(&cv.commitment().into(), &cmu); /// let outCiphertext = enc.encrypt_outgoing_plaintext(&cv.commitment().into(), &cmu);
/// ``` /// ```
pub struct SaplingNoteEncryption { pub struct SaplingNoteEncryption<R: RngCore + CryptoRng> {
epk: jubjub::SubgroupPoint, epk: jubjub::SubgroupPoint,
esk: jubjub::Fr, esk: jubjub::Fr,
note: Note, note: Note,
to: PaymentAddress, to: PaymentAddress,
memo: Memo, memo: Memo,
ovk: OutgoingViewingKey, /// `None` represents the `ovk = ⊥` case.
ovk: Option<OutgoingViewingKey>,
rng: R,
} }
impl SaplingNoteEncryption { impl<R: RngCore + CryptoRng> SaplingNoteEncryption<R> {
/// Creates a new encryption context for the given note. /// Creates a new encryption context for the given note.
pub fn new<R: RngCore + CryptoRng>( ///
ovk: OutgoingViewingKey, /// Setting `ovk` to `None` represents the `ovk = ⊥` case, where the note cannot be
/// recovered by the sender.
pub fn new(
ovk: Option<OutgoingViewingKey>,
note: Note, note: Note,
to: PaymentAddress, to: PaymentAddress,
memo: Memo, memo: Memo,
rng: &mut R, mut rng: R,
) -> SaplingNoteEncryption { ) -> Self {
let esk = note.generate_or_derive_esk(rng); let esk = note.generate_or_derive_esk(&mut rng);
let epk = note.g_d * esk; let epk = note.g_d * esk;
SaplingNoteEncryption { SaplingNoteEncryption {
@ -245,6 +270,7 @@ impl SaplingNoteEncryption {
to, to,
memo, memo,
ovk, ovk,
rng,
} }
} }
@ -297,20 +323,33 @@ impl SaplingNoteEncryption {
/// Generates `outCiphertext` for this note. /// Generates `outCiphertext` for this note.
pub fn encrypt_outgoing_plaintext( pub fn encrypt_outgoing_plaintext(
&self, &mut self,
cv: &jubjub::ExtendedPoint, cv: &jubjub::ExtendedPoint,
cmu: &bls12_381::Scalar, cmu: &bls12_381::Scalar,
) -> [u8; OUT_CIPHERTEXT_SIZE] { ) -> [u8; OUT_CIPHERTEXT_SIZE] {
let key = prf_ock(&self.ovk, &cv, &cmu, &self.epk); let (ock, input) = if let Some(ovk) = &self.ovk {
let ock = prf_ock(ovk, &cv, &cmu, &self.epk);
let mut input = [0u8; OUT_PLAINTEXT_SIZE]; let mut input = [0u8; OUT_PLAINTEXT_SIZE];
input[0..32].copy_from_slice(&self.note.pk_d.to_bytes()); input[0..32].copy_from_slice(&self.note.pk_d.to_bytes());
input[32..OUT_PLAINTEXT_SIZE].copy_from_slice(self.esk.to_repr().as_ref()); input[32..OUT_PLAINTEXT_SIZE].copy_from_slice(self.esk.to_repr().as_ref());
(ock, input)
} else {
// ovk = ⊥
let mut ock = OutgoingCipherKey([0; 32]);
let mut input = [0u8; OUT_PLAINTEXT_SIZE];
self.rng.fill_bytes(&mut ock.0);
self.rng.fill_bytes(&mut input);
(ock, input)
};
let mut output = [0u8; OUT_CIPHERTEXT_SIZE]; let mut output = [0u8; OUT_CIPHERTEXT_SIZE];
assert_eq!( assert_eq!(
ChachaPolyIetf::aead_cipher() ChachaPolyIetf::aead_cipher()
.seal_to(&mut output, &input, &[], key.as_bytes(), &[0u8; 12]) .seal_to(&mut output, &input, &[], ock.as_ref(), &[0u8; 12])
.unwrap(), .unwrap(),
OUT_CIPHERTEXT_SIZE OUT_CIPHERTEXT_SIZE
); );
@ -468,7 +507,7 @@ pub fn try_sapling_compact_note_decryption<P: consensus::Parameters>(
/// For decryption using a Full Viewing Key see [`try_sapling_output_recovery`]. /// For decryption using a Full Viewing Key see [`try_sapling_output_recovery`].
pub fn try_sapling_output_recovery_with_ock<P: consensus::Parameters>( pub fn try_sapling_output_recovery_with_ock<P: consensus::Parameters>(
height: u32, height: u32,
ock: &[u8], ock: &OutgoingCipherKey,
cmu: &bls12_381::Scalar, cmu: &bls12_381::Scalar,
epk: &jubjub::SubgroupPoint, epk: &jubjub::SubgroupPoint,
enc_ciphertext: &[u8], enc_ciphertext: &[u8],
@ -480,7 +519,7 @@ pub fn try_sapling_output_recovery_with_ock<P: consensus::Parameters>(
let mut op = [0; OUT_CIPHERTEXT_SIZE]; let mut op = [0; OUT_CIPHERTEXT_SIZE];
assert_eq!( assert_eq!(
ChachaPolyIetf::aead_cipher() ChachaPolyIetf::aead_cipher()
.open_to(&mut op, &out_ciphertext, &[], &ock, &[0u8; 12]) .open_to(&mut op, &out_ciphertext, &[], ock.as_ref(), &[0u8; 12])
.ok()?, .ok()?,
OUT_PLAINTEXT_SIZE OUT_PLAINTEXT_SIZE
); );
@ -583,7 +622,7 @@ pub fn try_sapling_output_recovery<P: consensus::Parameters>(
) -> Option<(Note, PaymentAddress, Memo)> { ) -> Option<(Note, PaymentAddress, Memo)> {
try_sapling_output_recovery_with_ock::<P>( try_sapling_output_recovery_with_ock::<P>(
height, height,
prf_ock(&ovk, &cv, &cmu, &epk).as_bytes(), &prf_ock(&ovk, &cv, &cmu, &epk),
cmu, cmu,
epk, epk,
enc_ciphertext, enc_ciphertext,
@ -593,7 +632,6 @@ pub fn try_sapling_output_recovery<P: consensus::Parameters>(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use blake2b_simd::Hash as Blake2bHash;
use crypto_api_chachapoly::ChachaPolyIetf; use crypto_api_chachapoly::ChachaPolyIetf;
use ff::{Field, PrimeField}; use ff::{Field, PrimeField};
use group::Group; use group::Group;
@ -606,8 +644,9 @@ mod tests {
use super::{ use super::{
kdf_sapling, prf_ock, sapling_ka_agree, try_sapling_compact_note_decryption, kdf_sapling, prf_ock, sapling_ka_agree, try_sapling_compact_note_decryption,
try_sapling_note_decryption, try_sapling_output_recovery, try_sapling_note_decryption, try_sapling_output_recovery,
try_sapling_output_recovery_with_ock, Memo, SaplingNoteEncryption, COMPACT_NOTE_SIZE, try_sapling_output_recovery_with_ock, Memo, OutgoingCipherKey, SaplingNoteEncryption,
ENC_CIPHERTEXT_SIZE, NOTE_PLAINTEXT_SIZE, OUT_CIPHERTEXT_SIZE, OUT_PLAINTEXT_SIZE, COMPACT_NOTE_SIZE, ENC_CIPHERTEXT_SIZE, NOTE_PLAINTEXT_SIZE, OUT_CIPHERTEXT_SIZE,
OUT_PLAINTEXT_SIZE,
}; };
use crate::{ use crate::{
consensus::{ consensus::{
@ -741,7 +780,7 @@ mod tests {
mut rng: &mut R, mut rng: &mut R,
) -> ( ) -> (
OutgoingViewingKey, OutgoingViewingKey,
Blake2bHash, OutgoingCipherKey,
jubjub::Fr, jubjub::Fr,
jubjub::ExtendedPoint, jubjub::ExtendedPoint,
bls12_381::Scalar, bls12_381::Scalar,
@ -782,7 +821,7 @@ mod tests {
); );
let ock_output_recovery = try_sapling_output_recovery_with_ock::<TestNetwork>( let ock_output_recovery = try_sapling_output_recovery_with_ock::<TestNetwork>(
height, height,
ock.as_bytes(), &ock,
&cmu, &cmu,
&epk, &epk,
&enc_ciphertext, &enc_ciphertext,
@ -801,7 +840,7 @@ mod tests {
mut rng: &mut R, mut rng: &mut R,
) -> ( ) -> (
OutgoingViewingKey, OutgoingViewingKey,
Blake2bHash, OutgoingCipherKey,
jubjub::Fr, jubjub::Fr,
jubjub::ExtendedPoint, jubjub::ExtendedPoint,
bls12_381::Scalar, bls12_381::Scalar,
@ -827,22 +866,13 @@ mod tests {
let cmu = note.cmu(); let cmu = note.cmu();
let ovk = OutgoingViewingKey([0; 32]); let ovk = OutgoingViewingKey([0; 32]);
let ne = SaplingNoteEncryption::new(ovk, note, pa, Memo([0; 512]), &mut rng); let mut ne = SaplingNoteEncryption::new(Some(ovk), note, pa, Memo([0; 512]), &mut rng);
let epk = ne.epk(); let epk = ne.epk().clone();
let enc_ciphertext = ne.encrypt_note_plaintext(); let enc_ciphertext = ne.encrypt_note_plaintext();
let out_ciphertext = ne.encrypt_outgoing_plaintext(&cv, &cmu); let out_ciphertext = ne.encrypt_outgoing_plaintext(&cv, &cmu);
let ock = prf_ock(&ovk, &cv, &cmu, &epk); let ock = prf_ock(&ovk, &cv, &cmu, &epk);
( (ovk, ock, ivk, cv, cmu, epk, enc_ciphertext, out_ciphertext)
ovk,
ock,
ivk,
cv,
cmu,
epk.clone(),
enc_ciphertext,
out_ciphertext,
)
} }
fn reencrypt_enc_ciphertext( fn reencrypt_enc_ciphertext(
@ -859,7 +889,7 @@ mod tests {
let mut op = [0; OUT_CIPHERTEXT_SIZE]; let mut op = [0; OUT_CIPHERTEXT_SIZE];
assert_eq!( assert_eq!(
ChachaPolyIetf::aead_cipher() ChachaPolyIetf::aead_cipher()
.open_to(&mut op, out_ciphertext, &[], ock.as_bytes(), &[0u8; 12]) .open_to(&mut op, out_ciphertext, &[], ock.as_ref(), &[0u8; 12])
.unwrap(), .unwrap(),
OUT_PLAINTEXT_SIZE OUT_PLAINTEXT_SIZE
); );
@ -1351,7 +1381,7 @@ mod tests {
assert_eq!( assert_eq!(
try_sapling_output_recovery_with_ock::<TestNetwork>( try_sapling_output_recovery_with_ock::<TestNetwork>(
height, height,
&[0u8; 32], &OutgoingCipherKey([0u8; 32]),
&cmu, &cmu,
&epk, &epk,
&enc_ciphertext, &enc_ciphertext,
@ -1416,7 +1446,7 @@ mod tests {
assert_eq!( assert_eq!(
try_sapling_output_recovery_with_ock::<TestNetwork>( try_sapling_output_recovery_with_ock::<TestNetwork>(
height, height,
&ock.as_bytes(), &ock,
&bls12_381::Scalar::random(&mut rng), &bls12_381::Scalar::random(&mut rng),
&epk, &epk,
&enc_ctext, &enc_ctext,
@ -1454,7 +1484,7 @@ mod tests {
assert_eq!( assert_eq!(
try_sapling_output_recovery_with_ock::<TestNetwork>( try_sapling_output_recovery_with_ock::<TestNetwork>(
height, height,
&ock.as_bytes(), &ock,
&cmu, &cmu,
&jubjub::SubgroupPoint::random(&mut rng), &jubjub::SubgroupPoint::random(&mut rng),
&enc_ciphertext, &enc_ciphertext,
@ -1493,7 +1523,7 @@ mod tests {
assert_eq!( assert_eq!(
try_sapling_output_recovery_with_ock::<TestNetwork>( try_sapling_output_recovery_with_ock::<TestNetwork>(
height, height,
&ock.as_bytes(), &ock,
&cmu, &cmu,
&epk, &epk,
&enc_ciphertext, &enc_ciphertext,
@ -1532,7 +1562,7 @@ mod tests {
assert_eq!( assert_eq!(
try_sapling_output_recovery_with_ock::<TestNetwork>( try_sapling_output_recovery_with_ock::<TestNetwork>(
height, height,
&ock.as_bytes(), &ock,
&cmu, &cmu,
&epk, &epk,
&enc_ciphertext, &enc_ciphertext,
@ -1582,7 +1612,7 @@ mod tests {
assert_eq!( assert_eq!(
try_sapling_output_recovery_with_ock::<TestNetwork>( try_sapling_output_recovery_with_ock::<TestNetwork>(
height, height,
&ock.as_bytes(), &ock,
&cmu, &cmu,
&epk, &epk,
&enc_ciphertext, &enc_ciphertext,
@ -1629,7 +1659,7 @@ mod tests {
assert_eq!( assert_eq!(
try_sapling_output_recovery_with_ock::<TestNetwork>( try_sapling_output_recovery_with_ock::<TestNetwork>(
height, height,
&ock.as_bytes(), &ock,
&cmu, &cmu,
&epk, &epk,
&enc_ciphertext, &enc_ciphertext,
@ -1676,7 +1706,7 @@ mod tests {
assert_eq!( assert_eq!(
try_sapling_output_recovery_with_ock::<TestNetwork>( try_sapling_output_recovery_with_ock::<TestNetwork>(
height, height,
&ock.as_bytes(), &ock,
&cmu, &cmu,
&epk, &epk,
&enc_ciphertext, &enc_ciphertext,
@ -1715,7 +1745,7 @@ mod tests {
assert_eq!( assert_eq!(
try_sapling_output_recovery_with_ock::<TestNetwork>( try_sapling_output_recovery_with_ock::<TestNetwork>(
height, height,
&ock.as_bytes(), &ock,
&cmu, &cmu,
&epk, &epk,
&enc_ciphertext, &enc_ciphertext,
@ -1776,7 +1806,7 @@ mod tests {
let ovk = OutgoingViewingKey(tv.ovk); let ovk = OutgoingViewingKey(tv.ovk);
let ock = prf_ock(&ovk, &cv, &cmu, &epk); let ock = prf_ock(&ovk, &cv, &cmu, &epk);
assert_eq!(ock.as_bytes(), tv.ock); assert_eq!(ock.as_ref(), tv.ock);
let to = PaymentAddress::from_parts(Diversifier(tv.default_d), pk_d).unwrap(); let to = PaymentAddress::from_parts(Diversifier(tv.default_d), pk_d).unwrap();
let note = to.create_note(tv.v, Rseed::BeforeZip212(rcm)).unwrap(); let note = to.create_note(tv.v, Rseed::BeforeZip212(rcm)).unwrap();
@ -1825,7 +1855,7 @@ mod tests {
// Test encryption // Test encryption
// //
let mut ne = SaplingNoteEncryption::new(ovk, note, to, Memo(tv.memo), &mut OsRng); let mut ne = SaplingNoteEncryption::new(Some(ovk), note, to, Memo(tv.memo), OsRng);
// Swap in the ephemeral keypair from the test vectors // Swap in the ephemeral keypair from the test vectors
ne.esk = esk; ne.esk = esk;
ne.epk = epk; ne.epk = epk;

View File

@ -76,7 +76,8 @@ struct SpendDescriptionInfo {
} }
pub struct SaplingOutput { pub struct SaplingOutput {
ovk: OutgoingViewingKey, /// `None` represents the `ovk = ⊥` case.
ovk: Option<OutgoingViewingKey>,
to: PaymentAddress, to: PaymentAddress,
note: Note, note: Note,
memo: Memo, memo: Memo,
@ -86,7 +87,7 @@ impl SaplingOutput {
pub fn new<R: RngCore + CryptoRng, P: consensus::Parameters>( pub fn new<R: RngCore + CryptoRng, P: consensus::Parameters>(
height: u32, height: u32,
rng: &mut R, rng: &mut R,
ovk: OutgoingViewingKey, ovk: Option<OutgoingViewingKey>,
to: PaymentAddress, to: PaymentAddress,
value: Amount, value: Amount,
memo: Option<Memo>, memo: Option<Memo>,
@ -122,7 +123,7 @@ impl SaplingOutput {
ctx: &mut P::SaplingProvingContext, ctx: &mut P::SaplingProvingContext,
rng: &mut R, rng: &mut R,
) -> OutputDescription { ) -> OutputDescription {
let encryptor = SaplingNoteEncryption::new( let mut encryptor = SaplingNoteEncryption::new(
self.ovk, self.ovk,
self.note.clone(), self.note.clone(),
self.to.clone(), self.to.clone(),
@ -396,7 +397,7 @@ impl<P: consensus::Parameters, R: RngCore + CryptoRng> Builder<P, R> {
/// Adds a Sapling address to send funds to. /// Adds a Sapling address to send funds to.
pub fn add_sapling_output( pub fn add_sapling_output(
&mut self, &mut self,
ovk: OutgoingViewingKey, ovk: Option<OutgoingViewingKey>,
to: PaymentAddress, to: PaymentAddress,
value: Amount, value: Amount,
memo: Option<Memo>, memo: Option<Memo>,
@ -502,7 +503,7 @@ impl<P: consensus::Parameters, R: RngCore + CryptoRng> Builder<P, R> {
return Err(Error::NoChangeAddress); return Err(Error::NoChangeAddress);
}; };
self.add_sapling_output(change_address.0, change_address.1, change, None)?; self.add_sapling_output(Some(change_address.0), change_address.1, change, None)?;
} }
// //
@ -728,7 +729,7 @@ mod tests {
let mut builder = Builder::<TestNetwork, OsRng>::new(0); let mut builder = Builder::<TestNetwork, OsRng>::new(0);
assert_eq!( assert_eq!(
builder.add_sapling_output(ovk, to, Amount::from_i64(-1).unwrap(), None), builder.add_sapling_output(Some(ovk), to, Amount::from_i64(-1).unwrap(), None),
Err(Error::InvalidAmount) Err(Error::InvalidAmount)
); );
} }
@ -840,7 +841,7 @@ mod tests {
} }
let extfvk = ExtendedFullViewingKey::from(&extsk); let extfvk = ExtendedFullViewingKey::from(&extsk);
let ovk = extfvk.fvk.ovk; let ovk = Some(extfvk.fvk.ovk);
let to = extfvk.default_address().unwrap().1; let to = extfvk.default_address().unwrap().1;
// Fail if there is only a Sapling output // Fail if there is only a Sapling output