use std::collections::{BTreeSet, HashMap};
use std::ops::Range;
use std::sync::Arc;
use tokio::sync::{RwLock, mpsc};
use zcash_keys::keys::UnifiedFullViewingKey;
use zcash_protocol::consensus::{self, BlockHeight};
use zcash_transparent::keys::NonHardenedChildIndex;
use zip32::AccountId;
use crate::client::{self, FetchRequest};
use crate::config::TransparentAddressDiscovery;
use crate::error::SyncError;
use crate::keys;
use crate::keys::transparent::{TransparentAddressId, TransparentScope};
use crate::wallet::traits::SyncWallet;
use crate::wallet::{KeyIdInterface, ScanTarget};
use super::MAX_REORG_ALLOWANCE;
pub(crate) async fn address_discovery<W: SyncWallet>(
consensus_parameters: &impl consensus::Parameters,
wallet: Arc<RwLock<W>>,
fetch_request_sender: mpsc::UnboundedSender<FetchRequest>,
ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
last_known_chain_height: BlockHeight,
chain_height: BlockHeight,
config: TransparentAddressDiscovery,
) -> Result<HashMap<String, TransparentAddressId>, SyncError<W::Error>> {
if !config.scopes.external && !config.scopes.internal && !config.scopes.refund {
return Ok(HashMap::new());
}
let wallet_guard = wallet.read().await;
let wallet_addresses = wallet_guard
.get_transparent_addresses()
.map_err(SyncError::WalletError)?
.clone();
let transparent_scan_floor = wallet_guard
.get_sync_state()
.map_err(SyncError::WalletError)?
.transparent_scan_floor;
drop(wallet_guard);
let mut scan_targets: BTreeSet<ScanTarget> = BTreeSet::new();
let block_range = Range {
start: discovery_start_height(
consensus_parameters,
last_known_chain_height,
transparent_scan_floor,
),
end: chain_height + 1,
};
for address in wallet_addresses.values() {
let transactions = client::get_transparent_address_transactions(
fetch_request_sender.clone(),
consensus_parameters,
address.clone(),
block_range.clone(),
)
.await?;
for (height, tx) in &transactions {
scan_targets.insert(ScanTarget::internal(*height, tx.txid()));
}
}
let mut scopes = Vec::new();
if config.scopes.external {
scopes.push(TransparentScope::External);
}
if config.scopes.internal {
scopes.push(TransparentScope::Internal);
}
if config.scopes.refund {
scopes.push(TransparentScope::Refund);
}
let mut gap_addresses = HashMap::new();
for (account_id, ufvk) in ufvks {
if let Some(account_pubkey) = ufvk.p2pkh() {
for scope in &scopes {
let mut address_index = if let Some(id) = wallet_addresses
.keys()
.rfind(|id| id.account_id() == *account_id && id.scope() == *scope)
{
id.address_index().next()
} else {
Some(NonHardenedChildIndex::ZERO)
}
.ok_or_else(|| {
SyncError::TransparentAddressDerivationError(bip32::Error::ChildNumber)
})?;
let mut unused_address_count: usize = 0;
let mut addresses: Vec<(TransparentAddressId, String)> = Vec::new();
while unused_address_count < config.gap_limit as usize {
let address_id = TransparentAddressId::new(*account_id, *scope, address_index);
let address = keys::transparent::derive_address(
consensus_parameters,
account_pubkey,
address_id,
)
.map_err(SyncError::TransparentAddressDerivationError)?;
addresses.push((address_id, address.clone()));
let transactions = client::get_transparent_address_transactions(
fetch_request_sender.clone(),
consensus_parameters,
address,
block_range.clone(),
)
.await?;
if transactions.is_empty() {
unused_address_count += 1;
} else {
for (height, tx) in &transactions {
scan_targets.insert(ScanTarget::internal(*height, tx.txid()));
}
unused_address_count = 0;
}
address_index = address_index.next().ok_or_else(|| {
SyncError::TransparentAddressDerivationError(bip32::Error::ChildNumber)
})?;
}
let gap_index = addresses.len().saturating_sub(config.gap_limit as usize);
let scope_gap_addresses: HashMap<String, TransparentAddressId> = addresses
.split_off(gap_index)
.into_iter()
.map(|(id, address)| (address, id))
.collect();
gap_addresses.extend(scope_gap_addresses);
let mut wallet_guard = wallet.write().await;
let wallet_addresses_mut = wallet_guard
.get_transparent_addresses_mut()
.map_err(SyncError::WalletError)?;
for (id, address) in addresses {
wallet_addresses_mut.insert(id, address);
}
}
}
}
let mut wallet_guard = wallet.write().await;
let sync_state = wallet_guard
.get_sync_state_mut()
.map_err(SyncError::WalletError)?;
for scan_target in scan_targets {
sync_state.insert_scan_target(scan_target);
}
wallet_guard
.set_save_flag()
.map_err(SyncError::WalletError)?;
Ok(gap_addresses)
}
fn discovery_start_height(
consensus_parameters: &impl consensus::Parameters,
last_known_chain_height: BlockHeight,
transparent_scan_floor: Option<BlockHeight>,
) -> BlockHeight {
let sapling_activation_height = consensus_parameters
.activation_height(consensus::NetworkUpgrade::Sapling)
.expect("sapling activation height should always return Some");
let reorg_start_height = last_known_chain_height.saturating_sub(MAX_REORG_ALLOWANCE) + 1;
transparent_scan_floor
.map_or(reorg_start_height, |floor| {
reorg_start_height.min(floor + 1)
})
.max(sapling_activation_height)
}
#[cfg(test)]
mod tests {
use zcash_protocol::consensus::{MAIN_NETWORK, Parameters as _};
use super::*;
const LAST_KNOWN_CHAIN_HEIGHT: u32 = 3_000_000;
const REORG_START_HEIGHT: BlockHeight =
BlockHeight::from_u32(LAST_KNOWN_CHAIN_HEIGHT - MAX_REORG_ALLOWANCE + 1);
const BLOCKS_MINED_BELOW_REORG_START: u32 = 500;
const BLOCKS_MINED_ABOVE_SAPLING_ACTIVATION: u32 = 10;
fn start_height(transparent_scan_floor: Option<BlockHeight>) -> BlockHeight {
discovery_start_height(
&MAIN_NETWORK,
BlockHeight::from_u32(LAST_KNOWN_CHAIN_HEIGHT),
transparent_scan_floor,
)
}
#[test]
fn discovery_starts_at_reorg_allowance_with_no_floor() {
assert_eq!(start_height(None), REORG_START_HEIGHT);
}
#[test]
fn discovery_starts_at_reorg_allowance_with_floor_above() {
for floor in [
REORG_START_HEIGHT,
BlockHeight::from_u32(LAST_KNOWN_CHAIN_HEIGHT),
] {
assert_eq!(start_height(Some(floor)), REORG_START_HEIGHT);
}
}
#[test]
fn discovery_starts_above_floor_below_reorg_allowance() {
let floor = REORG_START_HEIGHT - BLOCKS_MINED_BELOW_REORG_START;
assert_eq!(start_height(Some(floor)), floor + 1);
}
#[test]
fn discovery_starts_at_sapling_activation_at_the_lowest() {
let sapling_activation_height = MAIN_NETWORK
.activation_height(consensus::NetworkUpgrade::Sapling)
.unwrap();
assert_eq!(
discovery_start_height(
&MAIN_NETWORK,
sapling_activation_height + BLOCKS_MINED_ABOVE_SAPLING_ACTIVATION,
None
),
sapling_activation_height
);
}
}