build(ecc):Upgrade remaining ECC dependencies (#8568)

* upgrade zcash_client_backend for zebra-scan

* leave zebra-utils untouched

* remove unused

* upgrade zcash_primitives and orchard only for zebra-chain crate

* update and use TryFrom for amounts

* fix imports in serialize

* leave doc as it was

* leave doc as it was 2

* use `try_into` for amount

* use `zip_212_enforcement`

* updgrades primitives in zebra-rpc

* upgrade ecc dependencies for cargo-utils

* fix threading issue

* remove non needed Arc

* update deny.toml

* update primitives for zebra-grpc

* fix doc

* remove non needed single thread code in test

* cleanup some tests

* improve a bit the `ready_scan_block_keys` function

* clippy

* add spawn back in `scan_height_and_store_results`

* remove todo

* add note comment

* use a more explicit import of sapling stuff in serialize

* change(scan): Refactor scanning keys (#8577)

* Refactor converting dfvks to scanning keys

* Simplify handling of scanning keys

* Remove `test-dependencies` from the scanner reader

* Add comments

---------

Co-authored-by: Marek <mail@marek.onl>
This commit is contained in:
Alfredo Garcia 2024-06-04 18:04:40 -03:00 committed by GitHub
parent f2ab3271e9
commit dbff3b49bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 288 additions and 274 deletions

View File

@ -2839,9 +2839,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "orchard"
version = "0.6.0"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d31e68534df32024dcc89a8390ec6d7bef65edd87d91b45cfb481a2eb2d77c5"
checksum = "1fb255c3ffdccd3c84fe9ebed72aef64fdc72e6a3e4180dd411002d47abaad42"
dependencies = [
"aes",
"bitvec",
@ -2863,13 +2863,15 @@ dependencies = [
"subtle",
"tracing",
"zcash_note_encryption",
"zcash_spec",
"zip32",
]
[[package]]
name = "orchard"
version = "0.7.1"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fb255c3ffdccd3c84fe9ebed72aef64fdc72e6a3e4180dd411002d47abaad42"
checksum = "0462569fc8b0d1b158e4d640571867a4e4319225ebee2ab6647e60c70af19ae3"
dependencies = [
"aes",
"bitvec",
@ -4232,9 +4234,9 @@ dependencies = [
[[package]]
name = "shardtree"
version = "0.1.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19f96dde3a8693874f7e7c53d95616569b4009379a903789efbd448f4ea9cc7"
checksum = "3b3cdd24424ce0b381646737fedddc33c4dcf7dcd2d545056b53f7982097bef5"
dependencies = [
"bitflags 2.5.0",
"either",
@ -5743,24 +5745,27 @@ dependencies = [
[[package]]
name = "zcash_client_backend"
version = "0.10.0"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6a382af39be9ee5a3788157145c404b7cd19acc440903f6c34b09fb44f0e991"
checksum = "0364e69c446fcf96a1f73f342c6c3fa697ea65ae7eeeae7d76ca847b9c442e40"
dependencies = [
"base64 0.21.7",
"bech32",
"bls12_381",
"bs58",
"crossbeam-channel",
"document-features",
"group",
"hex",
"incrementalmerkletree",
"memuse",
"nom",
"orchard 0.6.0",
"nonempty",
"percent-encoding",
"prost",
"rand_core 0.6.4",
"rayon",
"sapling-crypto",
"secrecy",
"shardtree",
"subtle",
@ -5770,8 +5775,11 @@ dependencies = [
"which",
"zcash_address",
"zcash_encoding",
"zcash_keys",
"zcash_note_encryption",
"zcash_primitives 0.13.0",
"zcash_primitives 0.15.0",
"zcash_protocol",
"zip32",
]
[[package]]
@ -5795,6 +5803,32 @@ dependencies = [
"primitive-types",
]
[[package]]
name = "zcash_keys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "663489ffb4e51bc4436ff8796832612a9ff3c6516f1c620b5a840cb5dcd7b866"
dependencies = [
"bech32",
"blake2b_simd",
"bls12_381",
"bs58",
"document-features",
"group",
"memuse",
"nonempty",
"rand_core 0.6.4",
"sapling-crypto",
"secrecy",
"subtle",
"tracing",
"zcash_address",
"zcash_encoding",
"zcash_primitives 0.15.0",
"zcash_protocol",
"zip32",
]
[[package]]
name = "zcash_note_encryption"
version = "0.4.0"
@ -5808,42 +5842,6 @@ dependencies = [
"subtle",
]
[[package]]
name = "zcash_primitives"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d17e4c94ca8d69d2fcf2be97522da5732a580eb2125cda3b150761952f8df8e6"
dependencies = [
"aes",
"bip0039",
"bitvec",
"blake2b_simd",
"blake2s_simd",
"bls12_381",
"byteorder",
"equihash",
"ff",
"fpe",
"group",
"hdwallet",
"hex",
"incrementalmerkletree",
"jubjub",
"lazy_static",
"memuse",
"nonempty",
"orchard 0.6.0",
"rand 0.8.5",
"rand_core 0.6.4",
"ripemd",
"secp256k1",
"sha2",
"subtle",
"zcash_address",
"zcash_encoding",
"zcash_note_encryption",
]
[[package]]
name = "zcash_primitives"
version = "0.14.0"
@ -5882,6 +5880,42 @@ dependencies = [
"zip32",
]
[[package]]
name = "zcash_primitives"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a8d812efec385ecbcefc862c0005bb1336474ea7dd9b671d5bbddaadd04be2"
dependencies = [
"aes",
"bip0039",
"blake2b_simd",
"byteorder",
"document-features",
"equihash",
"ff",
"fpe",
"group",
"hex",
"incrementalmerkletree",
"jubjub",
"memuse",
"nonempty",
"orchard 0.8.0",
"rand 0.8.5",
"rand_core 0.6.4",
"redjubjub",
"sapling-crypto",
"sha2",
"subtle",
"tracing",
"zcash_address",
"zcash_encoding",
"zcash_note_encryption",
"zcash_protocol",
"zcash_spec",
"zip32",
]
[[package]]
name = "zcash_proofs"
version = "0.14.0"
@ -5991,7 +6025,7 @@ dependencies = [
"jubjub",
"lazy_static",
"num-integer",
"orchard 0.6.0",
"orchard 0.7.1",
"primitive-types",
"proptest",
"proptest-derive",
@ -6002,6 +6036,7 @@ dependencies = [
"reddsa",
"redjubjub",
"ripemd",
"sapling-crypto",
"secp256k1",
"serde",
"serde-big-array",
@ -6021,7 +6056,7 @@ dependencies = [
"zcash_encoding",
"zcash_history",
"zcash_note_encryption",
"zcash_primitives 0.13.0",
"zcash_primitives 0.14.0",
"zcash_protocol",
"zebra-test",
]
@ -6088,7 +6123,7 @@ dependencies = [
"tonic-build 0.11.0",
"tonic-reflection",
"tower",
"zcash_primitives 0.13.0",
"zcash_primitives 0.14.0",
"zebra-chain",
"zebra-node-services",
"zebra-state",
@ -6171,7 +6206,7 @@ dependencies = [
"tower",
"tracing",
"zcash_address",
"zcash_primitives 0.13.0",
"zcash_primitives 0.14.0",
"zebra-chain",
"zebra-consensus",
"zebra-network",
@ -6198,14 +6233,17 @@ dependencies = [
"proptest",
"proptest-derive",
"rand 0.8.5",
"sapling-crypto",
"semver 1.0.23",
"serde",
"tokio",
"tower",
"tracing",
"zcash_address",
"zcash_client_backend",
"zcash_keys",
"zcash_note_encryption",
"zcash_primitives 0.13.0",
"zcash_primitives 0.14.0",
"zebra-chain",
"zebra-grpc",
"zebra-node-services",
@ -6321,7 +6359,8 @@ dependencies = [
"tracing-error",
"tracing-subscriber",
"zcash_client_backend",
"zcash_primitives 0.13.0",
"zcash_primitives 0.15.0",
"zcash_protocol",
"zebra-chain",
"zebra-node-services",
"zebra-rpc",

View File

@ -84,9 +84,8 @@ skip-tree = [
# ECC crates
# wait for zcash_client_backend
{ name = "orchard", version = "=0.6.0" },
{ name = "zcash_primitives", version = "=0.13.0" },
{ name = "zcash_proofs", version = "=0.13.0" },
{ name = "orchard", version = "=0.7.0" },
{ name = "zcash_primitives", version = "=0.14.0" },
# wait for hdwallet to upgrade
{ name = "ring", version = "=0.16.20" },

View File

@ -93,11 +93,12 @@ x25519-dalek = { version = "2.0.1", features = ["serde"] }
# ECC deps
halo2 = { package = "halo2_proofs", version = "0.3.0" }
orchard = "0.6.0"
orchard = "0.7.0"
zcash_encoding = "0.2.0"
zcash_history = "0.4.0"
zcash_note_encryption = "0.4.0"
zcash_primitives = { version = "0.13.0", features = ["transparent-inputs"] }
zcash_primitives = { version = "0.14.0", features = ["transparent-inputs"] }
sapling = { package = "sapling-crypto", version = "0.1" }
zcash_protocol = { version = "0.1.1" }
zcash_address = { version = "0.3.2" }
@ -134,7 +135,7 @@ serde_json = { version = "1.0.117", optional = true }
tokio = { version = "1.37.0", optional = true }
# Experimental feature shielded-scan
zcash_client_backend = { version = "0.10.0-rc.1", optional = true }
zcash_client_backend = { version = "0.12.1", optional = true }
# Optional testing dependencies
proptest = { version = "1.4.0", optional = true }

View File

@ -30,7 +30,7 @@ pub const NETWORK_UPGRADES_IN_ORDER: [NetworkUpgrade; 8] = [
///
/// Network upgrades can change the Zcash network protocol or consensus rules in
/// incompatible ways.
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Ord, PartialOrd)]
#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))]
pub enum NetworkUpgrade {
/// The Zcash protocol for a Genesis block.

View File

@ -3,7 +3,6 @@
//! Usage: <https://docs.rs/zcash_address/0.2.0/zcash_address/trait.TryFromAddress.html#examples>
use zcash_address::unified::{self, Container};
use zcash_primitives::sapling;
use crate::{parameters::NetworkKind, transparent, BoxError};

View File

@ -1,11 +1,11 @@
//! Defines types and implements methods for parsing Sapling viewing keys and converting them to `zebra-chain` types
use zcash_client_backend::encoding::decode_extended_full_viewing_key;
use zcash_primitives::{
constants::*,
sapling::keys::{FullViewingKey as SaplingFvk, SaplingIvk},
zip32::DiversifiableFullViewingKey as SaplingDfvk,
use sapling::keys::{FullViewingKey as SaplingFvk, SaplingIvk};
use zcash_client_backend::{
encoding::decode_extended_full_viewing_key,
keys::sapling::DiversifiableFullViewingKey as SaplingDfvk,
};
use zcash_primitives::constants::*;
use crate::parameters::Network;

View File

@ -20,16 +20,22 @@ pub fn decrypts_successfully(transaction: &Transaction, network: &Network, heigh
let alt_tx = convert_tx_to_librustzcash(transaction, network_upgrade)
.expect("zcash_primitives and Zebra transaction formats must be compatible");
let alt_height = height.0.into();
let null_sapling_ovk = zcash_primitives::keys::OutgoingViewingKey([0u8; 32]);
let null_sapling_ovk = sapling::keys::OutgoingViewingKey([0u8; 32]);
// Note that, since this function is used to validate coinbase transactions, we can ignore
// the "grace period" mentioned in ZIP-212.
let zip_212_enforcement = if network_upgrade >= NetworkUpgrade::Canopy {
sapling::note_encryption::Zip212Enforcement::On
} else {
sapling::note_encryption::Zip212Enforcement::Off
};
if let Some(bundle) = alt_tx.sapling_bundle() {
for output in bundle.shielded_outputs().iter() {
let recovery = zcash_primitives::sapling::note_encryption::try_sapling_output_recovery(
network,
alt_height,
let recovery = sapling::note_encryption::try_sapling_output_recovery(
&null_sapling_ovk,
output,
zip_212_enforcement,
);
if recovery.is_none() {
return false;

View File

@ -29,13 +29,13 @@ impl zp_tx::components::transparent::Authorization for TransparentAuth<'_> {
// In this block we convert our Output to a librustzcash to TxOut.
// (We could do the serialize/deserialize route but it's simple enough to convert manually)
impl zp_tx::sighash::TransparentAuthorizingContext for TransparentAuth<'_> {
fn input_amounts(&self) -> Vec<zp_tx::components::amount::Amount> {
fn input_amounts(&self) -> Vec<zp_tx::components::amount::NonNegativeAmount> {
self.all_prev_outputs
.iter()
.map(|prevout| {
zp_tx::components::amount::Amount::from_nonnegative_i64_le_bytes(
prevout.value.to_bytes(),
)
prevout
.value
.try_into()
.expect("will not fail since it was previously validated")
})
.collect()
@ -83,39 +83,31 @@ impl<'a>
struct IdentityMap;
impl
zp_tx::components::sapling::MapAuth<
zp_tx::components::sapling::Authorized,
zp_tx::components::sapling::Authorized,
> for IdentityMap
impl zp_tx::components::sapling::MapAuth<sapling::bundle::Authorized, sapling::bundle::Authorized>
for IdentityMap
{
fn map_spend_proof(
&self,
p: <zp_tx::components::sapling::Authorized as zp_tx::components::sapling::Authorization>::SpendProof,
) -> <zp_tx::components::sapling::Authorized as zp_tx::components::sapling::Authorization>::SpendProof
{
&mut self,
p: <sapling::bundle::Authorized as sapling::bundle::Authorization>::SpendProof,
) -> <sapling::bundle::Authorized as sapling::bundle::Authorization>::SpendProof {
p
}
fn map_output_proof(
&self,
p: <zp_tx::components::sapling::Authorized as zp_tx::components::sapling::Authorization>::OutputProof,
) -> <zp_tx::components::sapling::Authorized as zp_tx::components::sapling::Authorization>::OutputProof
{
&mut self,
p: <sapling::bundle::Authorized as sapling::bundle::Authorization>::OutputProof,
) -> <sapling::bundle::Authorized as sapling::bundle::Authorization>::OutputProof {
p
}
fn map_auth_sig(
&self,
s: <zp_tx::components::sapling::Authorized as zp_tx::components::sapling::Authorization>::AuthSig,
) -> <zp_tx::components::sapling::Authorized as zp_tx::components::sapling::Authorization>::AuthSig{
&mut self,
s: <sapling::bundle::Authorized as sapling::bundle::Authorization>::AuthSig,
) -> <sapling::bundle::Authorized as sapling::bundle::Authorization>::AuthSig {
s
}
fn map_authorization(
&self,
a: zp_tx::components::sapling::Authorized,
) -> zp_tx::components::sapling::Authorized {
fn map_authorization(&mut self, a: sapling::bundle::Authorized) -> sapling::bundle::Authorized {
a
}
}
@ -141,7 +133,7 @@ struct PrecomputedAuth<'a> {
impl<'a> zp_tx::Authorization for PrecomputedAuth<'a> {
type TransparentAuth = TransparentAuth<'a>;
type SaplingAuth = zp_tx::components::sapling::Authorized;
type SaplingAuth = sapling::bundle::Authorized;
type OrchardAuth = orchard::bundle::Authorized;
}
@ -213,12 +205,12 @@ impl TryFrom<transparent::Output> for zp_tx::components::TxOut {
}
}
/// Convert a Zebra Amount into a librustzcash one.
impl TryFrom<Amount<NonNegative>> for zp_tx::components::Amount {
/// Convert a Zebra non-negative Amount into a librustzcash one.
impl TryFrom<Amount<NonNegative>> for zp_tx::components::amount::NonNegativeAmount {
type Error = ();
fn try_from(amount: Amount<NonNegative>) -> Result<Self, Self::Error> {
zp_tx::components::Amount::from_u64(amount.into())
zp_tx::components::amount::NonNegativeAmount::from_nonnegative_i64(amount.into())
}
}
@ -327,10 +319,10 @@ pub(crate) fn transparent_output_address(
let alt_addr = tx_out.recipient_address();
match alt_addr {
Some(zcash_primitives::legacy::TransparentAddress::PublicKey(pub_key_hash)) => Some(
Some(zcash_primitives::legacy::TransparentAddress::PublicKeyHash(pub_key_hash)) => Some(
transparent::Address::from_pub_key_hash(network.kind(), pub_key_hash),
),
Some(zcash_primitives::legacy::TransparentAddress::Script(script_hash)) => Some(
Some(zcash_primitives::legacy::TransparentAddress::ScriptHash(script_hash)) => Some(
transparent::Address::from_script_hash(network.kind(), script_hash),
),
None => None,

View File

@ -21,7 +21,7 @@ use crate::{
};
use super::*;
use sapling::{Output, SharedAnchor, Spend};
use crate::sapling;
impl ZcashDeserialize for jubjub::Fq {
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
@ -110,7 +110,7 @@ where
// range, so we can implement its serialization and deserialization separately.
// (Unlike V4, where it must be serialized as part of the transaction.)
impl ZcashSerialize for Option<sapling::ShieldedData<SharedAnchor>> {
impl ZcashSerialize for Option<sapling::ShieldedData<sapling::SharedAnchor>> {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
match self {
None => {
@ -127,21 +127,24 @@ impl ZcashSerialize for Option<sapling::ShieldedData<SharedAnchor>> {
}
}
impl ZcashSerialize for sapling::ShieldedData<SharedAnchor> {
impl ZcashSerialize for sapling::ShieldedData<sapling::SharedAnchor> {
fn zcash_serialize<W: io::Write>(&self, mut writer: W) -> Result<(), io::Error> {
// Collect arrays for Spends
// There's no unzip3, so we have to unzip twice.
let (spend_prefixes, spend_proofs_sigs): (Vec<_>, Vec<_>) = self
.spends()
.cloned()
.map(sapling::Spend::<SharedAnchor>::into_v5_parts)
.map(sapling::Spend::<sapling::SharedAnchor>::into_v5_parts)
.map(|(prefix, proof, sig)| (prefix, (proof, sig)))
.unzip();
let (spend_proofs, spend_sigs) = spend_proofs_sigs.into_iter().unzip();
// Collect arrays for Outputs
let (output_prefixes, output_proofs): (Vec<_>, _) =
self.outputs().cloned().map(Output::into_v5_parts).unzip();
let (output_prefixes, output_proofs): (Vec<_>, _) = self
.outputs()
.cloned()
.map(sapling::Output::into_v5_parts)
.unzip();
// Denoted as `nSpendsSapling` and `vSpendsSapling` in the spec.
spend_prefixes.zcash_serialize(&mut writer)?;
@ -175,7 +178,7 @@ impl ZcashSerialize for sapling::ShieldedData<SharedAnchor> {
// we can't split ShieldedData out of Option<ShieldedData> deserialization,
// because the counts are read along with the arrays.
impl ZcashDeserialize for Option<sapling::ShieldedData<SharedAnchor>> {
impl ZcashDeserialize for Option<sapling::ShieldedData<sapling::SharedAnchor>> {
#[allow(clippy::unwrap_in_result)]
fn zcash_deserialize<R: io::Read>(mut reader: R) -> Result<Self, SerializationError> {
// Denoted as `nSpendsSapling` and `vSpendsSapling` in the spec.
@ -269,14 +272,16 @@ impl ZcashDeserialize for Option<sapling::ShieldedData<SharedAnchor>> {
.into_iter()
.zip(spend_proofs)
.zip(spend_sigs)
.map(|((prefix, proof), sig)| Spend::<SharedAnchor>::from_v5_parts(prefix, proof, sig))
.map(|((prefix, proof), sig)| {
sapling::Spend::<sapling::SharedAnchor>::from_v5_parts(prefix, proof, sig)
})
.collect();
// Create shielded outputs from deserialized parts
let outputs = output_prefixes
.into_iter()
.zip(output_proofs)
.map(|(prefix, proof)| Output::from_v5_parts(prefix, proof))
.map(|(prefix, proof)| sapling::Output::from_v5_parts(prefix, proof))
.collect();
// Create transfers
@ -823,7 +828,7 @@ impl ZcashDeserialize for Transaction {
let shielded_outputs =
Vec::<sapling::OutputInTransactionV4>::zcash_deserialize(&mut limited_reader)?
.into_iter()
.map(Output::from_v4)
.map(sapling::Output::from_v4)
.collect();
// A bundle of fields denoted in the spec as `nJoinSplit`, `vJoinSplit`,

View File

@ -26,7 +26,7 @@ tokio-stream = "0.1.15"
tower = { version = "0.4.13", features = ["util", "buffer"] }
color-eyre = "0.6.3"
zcash_primitives = { version = "0.13.0" }
zcash_primitives = { version = "0.14.0" }
zebra-node-services = { path = "../zebra-node-services", version = "1.0.0-beta.37", features = ["shielded-scan"] }
zebra-chain = { path = "../zebra-chain" , version = "1.0.0-beta.37" }

View File

@ -64,7 +64,7 @@ tracing = "0.1.39"
hex = { version = "0.4.3", features = ["serde"] }
serde = { version = "1.0.203", features = ["serde_derive"] }
zcash_primitives = { version = "0.13.0" }
zcash_primitives = { version = "0.14.0" }
# Experimental feature getblocktemplate-rpcs
rand = { version = "0.8.5", optional = true }

View File

@ -51,8 +51,11 @@ tower = "0.4.13"
tracing = "0.1.39"
futures = "0.3.30"
zcash_client_backend = "0.10.0-rc.1"
zcash_primitives = "0.13.0"
zcash_client_backend = { version = "0.12.1" }
zcash_keys = { version = "0.2.0", features = ["sapling"] }
zcash_primitives = "0.14.0"
zcash_address = "0.3.2"
sapling = { package = "sapling-crypto", version = "0.1" }
zebra-chain = { path = "../zebra-chain", version = "1.0.0-beta.37", features = ["shielded-scan"] }
zebra-state = { path = "../zebra-state", version = "1.0.0-beta.37", features = ["shielded-scan"] }

View File

@ -23,4 +23,4 @@ pub mod tests;
pub use config::Config;
pub use init::{init_with_server, spawn_init};
pub use zcash_primitives::{sapling::SaplingIvk, zip32::DiversifiableFullViewingKey};
pub use sapling::{zip32::DiversifiableFullViewingKey, SaplingIvk};

View File

@ -8,12 +8,12 @@ use tokio::sync::{
oneshot,
};
use zcash_primitives::{sapling::SaplingIvk, zip32::DiversifiableFullViewingKey};
use sapling::zip32::DiversifiableFullViewingKey;
use zebra_chain::{block::Height, parameters::Network};
use zebra_node_services::scan_service::response::ScanResult;
use zebra_state::SaplingScanningKey;
use crate::scan::sapling_key_to_scan_block_keys;
use crate::scan::sapling_key_to_dfvk;
use super::ScanTask;
@ -57,17 +57,11 @@ impl ScanTask {
/// Returns newly registered keys for scanning.
pub fn process_messages(
cmd_receiver: &mut tokio::sync::mpsc::Receiver<ScanTaskCommand>,
registered_keys: &mut HashMap<
SaplingScanningKey,
(Vec<DiversifiableFullViewingKey>, Vec<SaplingIvk>),
>,
registered_keys: &mut HashMap<SaplingScanningKey, DiversifiableFullViewingKey>,
network: &Network,
) -> Result<
(
HashMap<
SaplingScanningKey,
(Vec<DiversifiableFullViewingKey>, Vec<SaplingIvk>, Height),
>,
HashMap<SaplingScanningKey, (DiversifiableFullViewingKey, Height)>,
HashMap<SaplingScanningKey, Sender<ScanResult>>,
Vec<(Receiver<ScanResult>, oneshot::Sender<Receiver<ScanResult>>)>,
),
@ -117,9 +111,9 @@ impl ScanTask {
sapling_activation_height
};
sapling_key_to_scan_block_keys(&key.0, network)
sapling_key_to_dfvk(&key.0, network)
.ok()
.map(|parsed| (key.0, (parsed.0, parsed.1, birth_height)))
.map(|parsed| (key.0, (parsed, birth_height)))
})
.collect();
@ -128,10 +122,7 @@ impl ScanTask {
new_keys.extend(keys.clone());
registered_keys.extend(
keys.into_iter()
.map(|(key, (dfvks, ivks, _))| (key, (dfvks, ivks))),
);
registered_keys.extend(keys.into_iter().map(|(key, (dfvk, _))| (key, dfvk)));
}
ScanTaskCommand::RemoveKeys { done_tx, keys } => {

View File

@ -15,18 +15,19 @@ use tokio::{
use tower::{buffer::Buffer, util::BoxService, Service, ServiceExt};
use tracing::Instrument;
use zcash_address::unified::{Encoding, Fvk, Ufvk};
use zcash_client_backend::{
data_api::ScannedBlock,
encoding::decode_extended_full_viewing_key,
keys::UnifiedFullViewingKey,
proto::compact_formats::{
ChainMetadata, CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx,
},
scanning::{ScanError, ScanningKey},
};
use zcash_primitives::{
sapling::SaplingIvk,
zip32::{AccountId, DiversifiableFullViewingKey, Scope},
scanning::{Nullifiers, ScanError, ScanningKeys},
};
use zcash_primitives::zip32::{AccountId, Scope};
use sapling::zip32::DiversifiableFullViewingKey;
use zebra_chain::{
block::{Block, Height},
@ -105,15 +106,9 @@ pub async fn start(
// Parse and convert keys once, then use them to scan all blocks.
// There is some cryptography here, but it should be fast even with thousands of keys.
let mut parsed_keys: HashMap<
SaplingScanningKey,
(Vec<DiversifiableFullViewingKey>, Vec<SaplingIvk>),
> = key_heights
let mut parsed_keys: HashMap<SaplingScanningKey, DiversifiableFullViewingKey> = key_heights
.keys()
.map(|key| {
let parsed_keys = sapling_key_to_scan_block_keys(key, &network)?;
Ok::<_, Report>((key.clone(), parsed_keys))
})
.map(|key| Ok::<_, Report>((key.clone(), sapling_key_to_dfvk(key, &network)?)))
.try_collect()?;
let mut subscribed_keys: HashMap<SaplingScanningKey, Sender<ScanResult>> = HashMap::new();
@ -165,7 +160,7 @@ pub async fn start(
let start_height = new_keys
.iter()
.map(|(_, (_, _, height))| *height)
.map(|(_, (_, height))| *height)
.min()
.unwrap_or(sapling_activation_height);
@ -251,7 +246,7 @@ pub async fn scan_height_and_store_results(
chain_tip_change: Option<ChainTipChange>,
storage: Storage,
key_last_scanned_heights: Arc<HashMap<SaplingScanningKey, Height>>,
parsed_keys: HashMap<SaplingScanningKey, (Vec<DiversifiableFullViewingKey>, Vec<SaplingIvk>)>,
parsed_keys: HashMap<String, DiversifiableFullViewingKey>,
subscribed_keys_receiver: watch::Receiver<Arc<HashMap<String, Sender<ScanResult>>>>,
) -> Result<Option<Height>, Report> {
let network = storage.network();
@ -277,8 +272,8 @@ pub async fn scan_height_and_store_results(
_ => unreachable!("unmatched response to a state::Block request"),
};
for (key_index_in_task, (sapling_key, (dfvks, ivks))) in parsed_keys.into_iter().enumerate() {
match key_last_scanned_heights.get(&sapling_key) {
for (key_index_in_task, (sapling_key, _)) in parsed_keys.iter().enumerate() {
match key_last_scanned_heights.get(sapling_key) {
// Only scan what was not scanned for each key
Some(last_scanned_height) if height <= *last_scanned_height => continue,
@ -312,6 +307,7 @@ pub async fn scan_height_and_store_results(
let block = block.clone();
let mut storage = storage.clone();
let network = network.clone();
let parsed_keys = parsed_keys.clone();
// We use a dummy size of the Sapling note commitment tree.
//
@ -326,19 +322,19 @@ pub async fn scan_height_and_store_results(
let sapling_tree_size = 1 << 16;
tokio::task::spawn_blocking(move || {
let dfvk_res =
scan_block(&network, &block, sapling_tree_size, &dfvks).map_err(|e| eyre!(e))?;
let ivk_res =
scan_block(&network, &block, sapling_tree_size, &ivks).map_err(|e| eyre!(e))?;
// TODO:
// - Wait until https://github.com/zcash/librustzcash/pull/1400 makes it to a release.
// - Create the scanning keys outside of this thread and move them here instead.
let scanning_keys = scanning_keys(parsed_keys.values()).expect("scanning keys");
let dfvk_res = scanned_block_to_db_result(dfvk_res);
let ivk_res = scanned_block_to_db_result(ivk_res);
let scanned_block = scan_block(&network, &block, sapling_tree_size, &scanning_keys)
.map_err(|e| eyre!(e))?;
let scanning_result = scanned_block_to_db_result(scanned_block);
let latest_subscribed_keys = subscribed_keys_receiver.borrow().clone();
if let Some(results_sender) = latest_subscribed_keys.get(&sapling_key).cloned() {
let results = dfvk_res.iter().chain(ivk_res.iter());
for (_tx_index, &tx_id) in results {
for (_tx_index, tx_id) in scanning_result.clone() {
// TODO: Handle `SendErrors` by dropping sender from `subscribed_keys`
let _ = results_sender.try_send(ScanResult {
key: sapling_key.clone(),
@ -348,9 +344,7 @@ pub async fn scan_height_and_store_results(
}
}
storage.add_sapling_results(&sapling_key, height, dfvk_res);
storage.add_sapling_results(&sapling_key, height, ivk_res);
storage.add_sapling_results(&sapling_key, height, scanning_result);
Ok::<_, Report>(())
})
.wait_for_panics()
@ -361,11 +355,6 @@ pub async fn scan_height_and_store_results(
}
/// Returns the transactions from `block` belonging to the given `scanning_keys`.
/// This list of keys should come from a single configured `SaplingScanningKey`.
///
/// For example, there are two individual viewing keys for most shielded transfers:
/// - the payment (external) key, and
/// - the change (internal) key.
///
/// # Performance / Hangs
///
@ -375,12 +364,12 @@ pub async fn scan_height_and_store_results(
/// TODO:
/// - Pass the real `sapling_tree_size` parameter from the state.
/// - Add other prior block metadata.
pub fn scan_block<K: ScanningKey>(
pub fn scan_block(
network: &Network,
block: &Block,
sapling_tree_size: u32,
scanning_keys: &[K],
) -> Result<ScannedBlock<K::Nf>, ScanError> {
scanning_key: &ScanningKeys<AccountId, (AccountId, Scope)>,
) -> Result<ScannedBlock<AccountId>, ScanError> {
// TODO: Implement a check that returns early when the block height is below the Sapling
// activation height.
@ -390,45 +379,28 @@ pub fn scan_block<K: ScanningKey>(
orchard_commitment_tree_size: 0,
};
// Use a dummy `AccountId` as we don't use accounts yet.
let dummy_account = AccountId::from(0);
let scanning_keys: Vec<_> = scanning_keys
.iter()
.map(|key| (&dummy_account, key))
.collect();
zcash_client_backend::scanning::scan_block(
network,
block_to_compact(block, chain_metadata),
scanning_keys.as_slice(),
scanning_key,
// Ignore whether notes are change from a viewer's own spends for now.
&[],
&Nullifiers::empty(),
// Ignore previous blocks for now.
None,
)
}
/// Converts a Zebra-format scanning key into some `scan_block()` keys.
///
/// Currently only accepts extended full viewing keys, and returns both their diversifiable full
/// viewing key and their individual viewing key, for testing purposes.
///
// TODO: work out what string format is used for SaplingIvk, if any, and support it here
// performance: stop returning both the dfvk and ivk for the same key
/// Converts a Zebra-format scanning key into diversifiable full viewing key.
// TODO: use `ViewingKey::parse` from zebra-chain instead
pub fn sapling_key_to_scan_block_keys(
pub fn sapling_key_to_dfvk(
key: &SaplingScanningKey,
network: &Network,
) -> Result<(Vec<DiversifiableFullViewingKey>, Vec<SaplingIvk>), Report> {
let efvk =
decode_extended_full_viewing_key(network.sapling_efvk_hrp(), key).map_err(|e| eyre!(e))?;
// Just return all the keys for now, so we can be sure our code supports them.
let dfvk = efvk.to_diversifiable_full_viewing_key();
let eivk = dfvk.to_ivk(Scope::External);
let iivk = dfvk.to_ivk(Scope::Internal);
Ok((vec![dfvk], vec![eivk, iivk]))
) -> Result<DiversifiableFullViewingKey, Report> {
Ok(
decode_extended_full_viewing_key(network.sapling_efvk_hrp(), key)
.map_err(|e| eyre!(e))?
.to_diversifiable_full_viewing_key(),
)
}
/// Converts a zebra block and meta data into a compact block.
@ -525,8 +497,8 @@ fn scanned_block_to_db_result<Nf>(
.iter()
.map(|tx| {
(
TransactionIndex::from_usize(tx.index),
SaplingScannedResult::from_bytes_in_display_order(*tx.txid.as_ref()),
TransactionIndex::from_usize(tx.block_index()),
SaplingScannedResult::from_bytes_in_display_order(*tx.txid().as_ref()),
)
})
.collect()
@ -565,3 +537,24 @@ pub fn spawn_init(
) -> JoinHandle<Result<(), Report>> {
tokio::spawn(start(state, chain_tip_change, storage, cmd_receiver).in_current_span())
}
/// Turns an iterator of [`DiversifiableFullViewingKey`]s to [`ScanningKeys`].
pub fn scanning_keys<'a>(
dfvks: impl IntoIterator<Item = &'a DiversifiableFullViewingKey>,
) -> Result<ScanningKeys<AccountId, (AccountId, Scope)>, Report> {
dfvks
.into_iter()
.enumerate()
.map(|(i, dfvk)| Ok((AccountId::try_from(u32::try_from(i)?)?, dfvk_to_ufvk(dfvk)?)))
.try_collect::<(_, _), Vec<(_, _)>, _>()
.map(ScanningKeys::from_account_ufvks)
}
/// Turns a [`DiversifiableFullViewingKey`] to [`UnifiedFullViewingKey`].
pub fn dfvk_to_ufvk(dfvk: &DiversifiableFullViewingKey) -> Result<UnifiedFullViewingKey, Report> {
UnifiedFullViewingKey::parse(&Ufvk::try_from_items(vec![Fvk::try_from((
2,
&dfvk.to_bytes()[..],
))?])?)
.map_err(|e| eyre!(e))
}

View File

@ -7,12 +7,12 @@ use crate::{
storage::Storage,
};
use color_eyre::eyre::Report;
use sapling::zip32::DiversifiableFullViewingKey;
use tokio::{
sync::{mpsc::Sender, watch},
task::JoinHandle,
};
use tracing::Instrument;
use zcash_primitives::{sapling::SaplingIvk, zip32::DiversifiableFullViewingKey};
use zebra_chain::block::Height;
use zebra_node_services::scan_service::response::ScanResult;
use zebra_state::SaplingScanningKey;
@ -24,7 +24,7 @@ pub struct ScanRangeTaskBuilder {
height_range: std::ops::Range<Height>,
/// The keys to be used for scanning blocks in this task
keys: HashMap<SaplingScanningKey, (Vec<DiversifiableFullViewingKey>, Vec<SaplingIvk>, Height)>,
keys: HashMap<SaplingScanningKey, (DiversifiableFullViewingKey, Height)>,
/// A handle to the state service for reading the blocks and the chain tip height
state: State,
@ -37,10 +37,7 @@ impl ScanRangeTaskBuilder {
/// Creates a new [`ScanRangeTaskBuilder`]
pub fn new(
stop_height: Height,
keys: HashMap<
SaplingScanningKey,
(Vec<DiversifiableFullViewingKey>, Vec<SaplingIvk>, Height),
>,
keys: HashMap<SaplingScanningKey, (DiversifiableFullViewingKey, Height)>,
state: State,
storage: Storage,
) -> Self {
@ -83,7 +80,7 @@ impl ScanRangeTaskBuilder {
// TODO: update the first parameter to `std::ops::Range<Height>`
pub async fn scan_range(
stop_before_height: Height,
keys: HashMap<SaplingScanningKey, (Vec<DiversifiableFullViewingKey>, Vec<SaplingIvk>, Height)>,
keys: HashMap<SaplingScanningKey, (DiversifiableFullViewingKey, Height)>,
state: State,
storage: Storage,
subscribed_keys_receiver: watch::Receiver<Arc<HashMap<String, Sender<ScanResult>>>>,
@ -99,7 +96,7 @@ pub async fn scan_range(
let key_heights: HashMap<String, Height> = keys
.iter()
.map(|(key, (_, _, height))| (key.clone(), *height))
.map(|(key, (_, height))| (key.clone(), *height))
.collect();
let mut height = get_min_height(&key_heights).unwrap_or(sapling_activation_height);
@ -107,12 +104,9 @@ pub async fn scan_range(
let key_heights = Arc::new(key_heights);
// Parse and convert keys once, then use them to scan all blocks.
let parsed_keys: HashMap<
SaplingScanningKey,
(Vec<DiversifiableFullViewingKey>, Vec<SaplingIvk>),
> = keys
let parsed_keys: HashMap<SaplingScanningKey, DiversifiableFullViewingKey> = keys
.into_iter()
.map(|(key, (decoded_dfvks, decoded_ivks, _h))| (key, (decoded_dfvks, decoded_ivks)))
.map(|(key, (dfvk, _))| (key, dfvk))
.collect();
while height < stop_before_height {

View File

@ -19,18 +19,14 @@ use zcash_client_backend::{
},
};
use zcash_note_encryption::Domain;
use zcash_primitives::{
block::BlockHash,
consensus::BlockHeight,
use zcash_primitives::{block::BlockHash, consensus::BlockHeight, memo::MemoBytes};
use ::sapling::{
constants::SPENDING_KEY_GENERATOR,
memo::MemoBytes,
sapling::{
note_encryption::{sapling_note_encryption, SaplingDomain},
util::generate_random_rseed,
value::NoteValue,
Note, Nullifier,
},
zip32,
zip32, Note, Nullifier,
};
use zebra_chain::{
@ -73,12 +69,12 @@ pub fn mock_sapling_scanning_keys(num_keys: u8, network: &Network) -> Vec<Saplin
keys
}
/// Generates an [`zip32::sapling::ExtendedFullViewingKey`] from `seed` for tests.
/// Generates an [`zip32::ExtendedFullViewingKey`] from `seed` for tests.
#[allow(deprecated)]
pub fn mock_sapling_efvk(seed: &[u8]) -> zip32::sapling::ExtendedFullViewingKey {
pub fn mock_sapling_efvk(seed: &[u8]) -> zip32::ExtendedFullViewingKey {
// TODO: Use `to_diversifiable_full_viewing_key` since `to_extended_full_viewing_key` is
// deprecated.
zip32::sapling::ExtendedSpendingKey::master(seed).to_extended_full_viewing_key()
zip32::ExtendedSpendingKey::master(seed).to_extended_full_viewing_key()
}
/// Generates a fake block containing a Sapling output decryptable by `dfvk`.
@ -91,7 +87,7 @@ pub fn mock_sapling_efvk(seed: &[u8]) -> zip32::sapling::ExtendedFullViewingKey
pub fn fake_block(
height: BlockHeight,
nf: Nullifier,
dfvk: &zip32::sapling::DiversifiableFullViewingKey,
dfvk: &zip32::DiversifiableFullViewingKey,
value: u64,
tx_after: bool,
initial_sapling_tree_size: Option<u32>,
@ -165,7 +161,7 @@ pub fn fake_compact_block(
height: BlockHeight,
prev_hash: BlockHash,
nf: Nullifier,
dfvk: &zip32::sapling::DiversifiableFullViewingKey,
dfvk: &zip32::DiversifiableFullViewingKey,
value: u64,
tx_after: bool,
initial_sapling_tree_size: Option<u32>,
@ -174,23 +170,17 @@ pub fn fake_compact_block(
// Create a fake Note for the account
let mut rng = OsRng;
let rseed = generate_random_rseed(
&zcash_primitives::consensus::Network::TestNetwork,
height,
&mut rng,
);
let rseed = generate_random_rseed(::sapling::note_encryption::Zip212Enforcement::Off, &mut rng);
let note = Note::from_parts(to, NoteValue::from_raw(value), rseed);
let encryptor = sapling_note_encryption::<_, zcash_primitives::consensus::Network>(
let encryptor = sapling_note_encryption::<_>(
Some(dfvk.fvk().ovk),
note.clone(),
MemoBytes::empty(),
*MemoBytes::empty().as_array(),
&mut rng,
);
let cmu = note.cmu().to_bytes().to_vec();
let ephemeral_key =
SaplingDomain::<zcash_primitives::consensus::Network>::epk_bytes(encryptor.epk())
.0
.to_vec();
let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk()).0.to_vec();
let enc_ciphertext = encryptor.encrypt_note_plaintext();
// Create a fake CompactBlock containing the note

View File

@ -4,15 +4,15 @@ use std::sync::Arc;
use color_eyre::Result;
use sapling::{
zip32::{DiversifiableFullViewingKey, ExtendedSpendingKey},
Nullifier,
};
use zcash_client_backend::{
encoding::{decode_extended_full_viewing_key, encode_extended_full_viewing_key},
proto::compact_formats::ChainMetadata,
};
use zcash_primitives::{
constants::mainnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY,
sapling::Nullifier,
zip32::{DiversifiableFullViewingKey, ExtendedSpendingKey},
};
use zcash_primitives::constants::mainnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY;
use zebra_chain::{
block::{Block, Height},
@ -23,7 +23,7 @@ use zebra_chain::{
use zebra_state::{SaplingScannedResult, TransactionIndex};
use crate::{
scan::{block_to_compact, scan_block},
scan::{block_to_compact, scan_block, scanning_keys},
storage::db::tests::new_test_storage,
tests::{fake_block, mock_sapling_efvk, ZECPAGES_SAPLING_VIEWING_KEY},
};
@ -42,7 +42,9 @@ async fn scanning_from_fake_generated_blocks() -> Result<()> {
assert_eq!(block.transactions.len(), 4);
let res = scan_block(&Network::Mainnet, &block, sapling_tree_size, &[&dfvk]).unwrap();
let scanning_keys = scanning_keys(&vec![dfvk]).expect("scanning key");
let res = scan_block(&Network::Mainnet, &block, sapling_tree_size, &scanning_keys).unwrap();
// The response should have one transaction relevant to the key we provided.
assert_eq!(res.transactions().len(), 1);
@ -52,11 +54,11 @@ async fn scanning_from_fake_generated_blocks() -> Result<()> {
.transactions
.iter()
.map(|tx| tx.hash().bytes_in_display_order())
.any(|txid| &txid == res.transactions()[0].txid.as_ref()));
.any(|txid| &txid == res.transactions()[0].txid().as_ref()));
// Check that the txid in the scanning result matches the third tx in the original block.
assert_eq!(
res.transactions()[0].txid.as_ref(),
res.transactions()[0].txid().as_ref(),
&block.transactions[2].hash().bytes_in_display_order()
);
@ -78,10 +80,7 @@ async fn scanning_zecpages_from_populated_zebra_state() -> Result<()> {
)
.unwrap();
// Build a vector of viewing keys `vks` to scan for.
let fvk = efvk.fvk;
let ivk = fvk.vk.ivk();
let ivks = vec![ivk];
let dfvk = efvk.to_diversifiable_full_viewing_key();
let network = Network::Mainnet;
@ -103,6 +102,9 @@ async fn scanning_zecpages_from_populated_zebra_state() -> Result<()> {
let mut transactions_found = 0;
let mut transactions_scanned = 0;
let mut blocks_scanned = 0;
let scanning_keys = scanning_keys(&vec![dfvk]).expect("scanning key");
while let Some(block) = db.block(height.into()) {
// We use a dummy size of the Sapling note commitment tree. We can't set the size to zero
// because the underlying scanning function would return
@ -118,7 +120,12 @@ async fn scanning_zecpages_from_populated_zebra_state() -> Result<()> {
let compact_block = block_to_compact(&block, chain_metadata);
let res = scan_block(&network, &block, sapling_commitment_tree_size, &ivks)
let res = scan_block(
&network,
&block,
sapling_commitment_tree_size,
&scanning_keys,
)
.expect("scanning block for the ZECpages viewing key should work");
transactions_found += res.transactions().len();
@ -177,13 +184,16 @@ fn scanning_fake_blocks_store_key_and_results() -> Result<()> {
let (block, sapling_tree_size) = fake_block(1u32.into(), nf, &dfvk, 1, true, Some(0));
let result = scan_block(&Network::Mainnet, &block, sapling_tree_size, &[&dfvk]).unwrap();
let scanning_keys = scanning_keys(&vec![dfvk]).expect("scanning key");
let result = scan_block(&Network::Mainnet, &block, sapling_tree_size, &scanning_keys).unwrap();
// The response should have one transaction relevant to the key we provided.
assert_eq!(result.transactions().len(), 1);
let result =
SaplingScannedResult::from_bytes_in_display_order(*result.transactions()[0].txid.as_ref());
let result = SaplingScannedResult::from_bytes_in_display_order(
*result.transactions()[0].txid().as_ref(),
);
// Add result to database
storage.add_sapling_results(

View File

@ -120,8 +120,9 @@ tokio = { version = "1.37.0", features = ["full"], optional = true }
jsonrpc = { version = "0.18.0", optional = true }
zcash_primitives = { version = "0.13.0", optional = true }
zcash_client_backend = {version = "0.10.0-rc.1", optional = true}
zcash_primitives = { version = "0.15.0", optional = true }
zcash_client_backend = { version = "0.12.1", optional = true }
zcash_protocol = { version = "0.1.1" }
# For the openapi generator
syn = { version = "2.0.66", features = ["full"], optional = true }

View File

@ -8,17 +8,15 @@
use std::collections::HashMap;
use hex::ToHex;
use itertools::Itertools;
use jsonrpc::simple_http::SimpleHttpTransport;
use jsonrpc::Client;
use zcash_client_backend::decrypt_transaction;
use zcash_client_backend::keys::UnifiedFullViewingKey;
use zcash_primitives::consensus::{BlockHeight, BranchId};
use zcash_primitives::transaction::Transaction;
use zcash_primitives::zip32::AccountId;
use zebra_scan::scan::sapling_key_to_scan_block_keys;
use zebra_scan::scan::{dfvk_to_ufvk, sapling_key_to_dfvk};
use zebra_scan::{storage::Storage, Config};
/// Prints the memos of transactions from Zebra's scanning results storage.
@ -47,20 +45,13 @@ pub fn main() {
let mut prev_memo = "".to_owned();
for (key, _) in storage.sapling_keys_last_heights().iter() {
let dfvk = sapling_key_to_scan_block_keys(key, &network)
.expect("Scanning key from the storage should be valid")
.0
.into_iter()
.exactly_one()
.expect("There should be exactly one dfvk");
let ufvk_with_acc_id = HashMap::from([(
AccountId::from(1),
UnifiedFullViewingKey::new(Some(dfvk), None).expect("`dfvk` should be `Some`"),
let ufvks = HashMap::from([(
AccountId::ZERO,
dfvk_to_ufvk(&sapling_key_to_dfvk(key, &network).expect("dfvk")).expect("ufvk"),
)]);
for (height, txids) in storage.sapling_results(key) {
let height = BlockHeight::from(height);
let height = BlockHeight::from(height.0);
for txid in txids.iter() {
let tx = Transaction::read(
@ -70,8 +61,8 @@ pub fn main() {
)
.expect("TX fetched via RPC should be deserializable from raw bytes");
for output in decrypt_transaction(&network, height, &tx, &ufvk_with_acc_id) {
let memo = memo_bytes_to_string(output.memo.as_array());
for output in decrypt_transaction(&network, height, &tx, &ufvks).sapling_outputs() {
let memo = memo_bytes_to_string(output.memo().as_array());
if !memo.is_empty()
// Filter out some uninteresting and repeating memos from ZECPages.