use std::{collections::BTreeMap, hash::Hash, ops::Range};
#[cfg(feature = "orchard")]
use {
crate::data_api::ORCHARD_SHARD_HEIGHT, shardtree::store::Checkpoint, std::collections::BTreeSet,
};
use rayon::{
iter::{IndexedParallelIterator as _, ParallelIterator},
slice::ParallelSliceMut as _,
};
use tracing::{debug, info, trace, warn};
use incrementalmerkletree::{Hashable, Marking, Position, Retention, frontier::Frontier};
use shardtree::{LocatedPrunableTree, ShardTree, error::ShardTreeError, store::ShardStore};
use transparent::{address::TransparentAddress, bundle::OutPoint};
use zcash_keys::{address::Receiver, encoding::AddressCodec as _};
use zcash_primitives::transaction::Transaction;
use zcash_protocol::{
PoolType, ShieldedPool,
consensus::{self, BlockHeight},
value::{BalanceError, Zatoshis},
};
use zcash_script::solver::ScriptKind;
use crate::{
TransferType,
data_api::{
DecryptedTransaction, SAPLING_SHARD_HEIGHT, ScannedBlock, TransactionStatus,
WalletCommitmentTrees, anchor_retention::AnchorRetention, chain::ChainState,
ll::ReceivedShieldedOutput,
},
wallet::{Recipient, WalletTransparentOutput},
};
use super::{LowLevelWalletRead, LowLevelWalletWrite, TxMeta};
#[cfg(feature = "orchard")]
use crate::data_api::anchor_retention::{AnchorRetentionInterval, PoolMigrationParams};
#[cfg(feature = "transparent-inputs")]
use {
crate::data_api::Account,
std::collections::HashSet,
transparent::keys::TransparentKeyScope,
zcash_keys::keys::{
ReceiverRequirement::*,
UnifiedAddressRequest,
transparent::gap_limits::{
AddressStore, GapAddressesError, GapLimits, generate_gap_addresses,
},
},
};
pub(crate) const PRUNING_DEPTH: u32 = 100;
pub(crate) fn determine_fee<DbT, T: TxMeta>(
_wallet_db: &DbT,
tx: &T,
) -> Result<Option<Zatoshis>, DbT::Error>
where
DbT: LowLevelWalletRead,
DbT::Error: From<BalanceError>,
{
tx.fee_paid(|_outpoint| {
#[cfg(not(feature = "transparent-inputs"))]
{
Ok(None)
}
#[cfg(feature = "transparent-inputs")]
if let Some(out) = _wallet_db.get_wallet_transparent_output(_outpoint, None)? {
Ok(Some(out.txout().value()))
} else {
Ok(None)
}
})
}
#[cfg(feature = "transparent-inputs")]
pub fn generate_transparent_gap_addresses<DbT, SE>(
wallet_db: &mut DbT,
gap_limits: GapLimits,
account_id: <DbT as LowLevelWalletRead>::AccountId,
key_scope: TransparentKeyScope,
request: UnifiedAddressRequest,
) -> Result<(), GapAddressesError<SE>>
where
DbT: LowLevelWalletWrite<Error = SE>
+ AddressStore<Error = SE, AccountRef = <DbT as LowLevelWalletRead>::AccountRef>,
DbT::TxRef: Eq + Hash,
{
let account_ref = wallet_db
.get_account_ref(account_id)
.map_err(GapAddressesError::Storage)?;
let account = wallet_db
.get_account_internal(account_ref)
.map_err(GapAddressesError::Storage)?
.ok_or(GapAddressesError::AccountUnknown)?;
generate_gap_addresses(
wallet_db,
&gap_limits,
account_ref,
&account.uivk(),
account.ufvk(),
key_scope,
request,
false,
)?;
Ok(())
}
#[derive(Debug)]
#[non_exhaustive]
pub enum PutBlocksError<SE, TE> {
NonSequentialBlocks {
prev_height: BlockHeight,
block_height: BlockHeight,
},
Storage(SE),
ShardTree(ShardTreeError<TE>),
ShardTreeForBlockRange {
pool: ShieldedPool,
block_range: Range<BlockHeight>,
error: ShardTreeError<TE>,
},
#[cfg(feature = "transparent-inputs")]
GapAddresses(GapAddressesError<SE>),
}
impl<SE, TE> From<ShardTreeError<TE>> for PutBlocksError<SE, TE> {
fn from(value: ShardTreeError<TE>) -> Self {
PutBlocksError::ShardTree(value)
}
}
#[cfg(feature = "transparent-inputs")]
impl<SE, TE> From<GapAddressesError<SE>> for PutBlocksError<SE, TE> {
fn from(value: GapAddressesError<SE>) -> Self {
PutBlocksError::GapAddresses(value)
}
}
#[cfg(not(feature = "transparent-inputs"))]
pub trait PutBlocksDbT<SE, TE, AR>:
LowLevelWalletWrite<Error = SE> + WalletCommitmentTrees<Error = TE>
{
}
#[cfg(not(feature = "transparent-inputs"))]
impl<T: LowLevelWalletWrite<Error = SE> + WalletCommitmentTrees<Error = TE>, SE, TE, AR>
PutBlocksDbT<SE, TE, AR> for T
{
}
#[cfg(feature = "transparent-inputs")]
pub trait PutBlocksDbT<SE, TE, AR>:
LowLevelWalletWrite<Error = SE>
+ WalletCommitmentTrees<Error = TE>
+ AddressStore<Error = SE, AccountRef = AR>
{
}
#[cfg(feature = "transparent-inputs")]
impl<
T: LowLevelWalletWrite<Error = SE>
+ WalletCommitmentTrees<Error = TE>
+ AddressStore<Error = SE, AccountRef = AR>,
SE,
TE,
AR,
> PutBlocksDbT<SE, TE, AR> for T
{
}
#[cfg(not(feature = "transparent-inputs"))]
pub trait PutBlocksRowsDbT<SE, AR>: LowLevelWalletWrite<Error = SE> {}
#[cfg(not(feature = "transparent-inputs"))]
impl<T: LowLevelWalletWrite<Error = SE>, SE, AR> PutBlocksRowsDbT<SE, AR> for T {}
#[cfg(feature = "transparent-inputs")]
pub trait PutBlocksRowsDbT<SE, AR>:
LowLevelWalletWrite<Error = SE> + AddressStore<Error = SE, AccountRef = AR>
{
}
#[cfg(feature = "transparent-inputs")]
impl<T: LowLevelWalletWrite<Error = SE> + AddressStore<Error = SE, AccountRef = AR>, SE, AR>
PutBlocksRowsDbT<SE, AR> for T
{
}
#[derive(Default)]
pub struct PutBlocksRows {
pub sapling_commitments: Vec<Option<(sapling::Node, Retention<BlockHeight>)>>,
#[cfg(feature = "orchard")]
pub orchard_commitments:
Vec<Option<(orchard::tree::MerkleHashOrchard, Retention<BlockHeight>)>>,
#[cfg(feature = "orchard")]
pub ironwood_commitments:
Vec<Option<(orchard::tree::MerkleHashOrchard, Retention<BlockHeight>)>>,
pub note_positions: Vec<(ShieldedPool, Position)>,
pub last_scanned_height: Option<BlockHeight>,
}
pub fn put_blocks_rows<DbT, SE, TE>(
wallet_db: &mut DbT,
#[cfg(feature = "transparent-inputs")] gap_limits: GapLimits,
from_state: &ChainState,
blocks: Vec<ScannedBlock<<DbT as LowLevelWalletRead>::AccountId>>,
) -> Result<PutBlocksRows, PutBlocksError<SE, TE>>
where
DbT: PutBlocksRowsDbT<SE, <DbT as LowLevelWalletRead>::AccountRef>,
DbT::TxRef: Eq + Hash,
{
if blocks.is_empty() {
return Ok(PutBlocksRows::default());
}
let initial_block = blocks.first().expect("blocks is known to be nonempty");
let mut initial_block_sequential = from_state.block_height() + 1 == initial_block.height();
{
initial_block_sequential &= from_state.final_sapling_tree().tree_size()
+ u64::try_from(initial_block.sapling().commitments().len()).unwrap()
== u64::from(initial_block.sapling().final_tree_size());
}
#[cfg(feature = "orchard")]
{
initial_block_sequential &= from_state.final_orchard_tree().tree_size()
+ u64::try_from(initial_block.orchard().commitments().len()).unwrap()
== u64::from(initial_block.orchard().final_tree_size());
initial_block_sequential &= from_state.final_ironwood_tree().tree_size()
+ u64::try_from(initial_block.ironwood().commitments().len()).unwrap()
== u64::from(initial_block.ironwood().final_tree_size());
}
if !initial_block_sequential {
return Err(PutBlocksError::NonSequentialBlocks {
prev_height: from_state.block_height(),
block_height: initial_block.height(),
});
}
let nullifier_tracking_floor = nullifier_tracking_floor(
wallet_db
.block_fully_scanned_height()
.map_err(PutBlocksError::Storage)?,
from_state.block_height(),
blocks.last().map(|block| block.height()),
);
let mut sapling_commitments = vec![];
#[cfg(feature = "orchard")]
let mut orchard_commitments = vec![];
#[cfg(feature = "orchard")]
let mut ironwood_commitments = vec![];
let mut last_scanned_height = None;
let mut note_positions = vec![];
#[cfg(feature = "transparent-inputs")]
let mut tx_refs = HashSet::new();
for block in blocks.into_iter() {
if last_scanned_height
.iter()
.any(|prev| block.height() != *prev + 1)
{
return Err(PutBlocksError::NonSequentialBlocks {
prev_height: last_scanned_height.expect("last scanned height is known"),
block_height: block.height(),
});
}
wallet_db
.put_block_meta(
block.height(),
block.block_hash(),
block.block_time(),
block.sapling().final_tree_size(),
block.sapling().commitments().len().try_into().unwrap(),
#[cfg(feature = "orchard")]
block.orchard().final_tree_size(),
#[cfg(feature = "orchard")]
block.orchard().commitments().len().try_into().unwrap(),
#[cfg(feature = "orchard")]
block.ironwood().final_tree_size(),
#[cfg(feature = "orchard")]
block.ironwood().commitments().len().try_into().unwrap(),
)
.map_err(PutBlocksError::Storage)?;
for tx in block.transactions() {
let tx_ref = wallet_db
.put_tx_meta(tx, block.height())
.map_err(PutBlocksError::Storage)?;
#[cfg(feature = "transparent-inputs")]
tx_refs.insert(tx_ref);
wallet_db
.queue_tx_retrieval(std::iter::once(tx.txid()), None)
.map_err(PutBlocksError::Storage)?;
let _ = mark_notes_spent(
wallet_db,
tx_ref,
#[cfg(feature = "transparent-inputs")]
None.iter(),
tx.sapling_spends().iter().map(|spend| spend.nf()),
#[cfg(feature = "orchard")]
tx.orchard_spends().iter().map(|spend| spend.nf()),
#[cfg(feature = "orchard")]
tx.ironwood_spends().iter().map(|spend| spend.nf()),
)
.map_err(PutBlocksError::Storage)?;
let params: Option<&consensus::Network> = None;
put_shielded_outputs(
wallet_db,
params,
tx_ref,
None,
tx.sapling_outputs(),
|wallet_db, output| {
Ok(output
.nf()
.map(|nf| wallet_db.detect_sapling_spend(nf))
.transpose()?
.flatten())
},
|wallet_db, output, tx_ref, spent_in| {
wallet_db.put_received_sapling_note(
output,
tx_ref,
Some(block.height()),
spent_in,
)
},
|_account_id| (),
)
.map_err(PutBlocksError::Storage)?;
#[cfg(feature = "orchard")]
put_shielded_outputs(
wallet_db,
params,
tx_ref,
None,
tx.orchard_outputs(),
|wallet_db, output| {
Ok(output
.nf()
.map(|nf| wallet_db.detect_orchard_spend(nf))
.transpose()?
.flatten())
},
|wallet_db, output, tx_ref, spent_in| {
wallet_db.put_received_orchard_note(
output,
tx_ref,
Some(block.height()),
spent_in,
)
},
|_account_id| (),
)
.map_err(PutBlocksError::Storage)?;
#[cfg(feature = "orchard")]
put_shielded_outputs(
wallet_db,
params,
tx_ref,
None,
tx.ironwood_outputs(),
|wallet_db, output| {
Ok(output
.nf()
.map(|nf| wallet_db.detect_ironwood_spend(nf))
.transpose()?
.flatten())
},
|wallet_db, output, tx_ref, spent_in| {
wallet_db.put_received_ironwood_note(
output,
tx_ref,
Some(block.height()),
spent_in,
)
},
|_account_id| (),
)
.map_err(PutBlocksError::Storage)?;
}
if should_track_nullifiers(nullifier_tracking_floor, block.height()) {
wallet_db
.track_block_sapling_nullifiers(block.height(), block.sapling().nullifier_map())
.map_err(PutBlocksError::Storage)?;
#[cfg(feature = "orchard")]
wallet_db
.track_block_orchard_nullifiers(block.height(), block.orchard().nullifier_map())
.map_err(PutBlocksError::Storage)?;
#[cfg(feature = "orchard")]
wallet_db
.track_block_ironwood_nullifiers(block.height(), block.ironwood().nullifier_map())
.map_err(PutBlocksError::Storage)?;
}
note_positions.extend(block.transactions().iter().flat_map(|wtx| {
let iter = wtx
.sapling_outputs()
.iter()
.map(|out| (ShieldedPool::Sapling, out.note_commitment_tree_position()));
#[cfg(feature = "orchard")]
let iter = iter.chain(
wtx.orchard_outputs()
.iter()
.map(|out| (ShieldedPool::Orchard, out.note_commitment_tree_position())),
);
#[cfg(feature = "orchard")]
let iter = iter.chain(
wtx.ironwood_outputs()
.iter()
.map(|out| (ShieldedPool::Ironwood, out.note_commitment_tree_position())),
);
iter
}));
last_scanned_height = Some(block.height());
let block_commitments = block.into_commitments();
trace!(
"Sapling commitments for {:?}: {:?}",
last_scanned_height,
block_commitments
.sapling
.iter()
.map(|(_, r)| *r)
.collect::<Vec<_>>()
);
#[cfg(feature = "orchard")]
trace!(
"Orchard commitments for {:?}: {:?}",
last_scanned_height,
block_commitments
.orchard
.iter()
.map(|(_, r)| *r)
.collect::<Vec<_>>()
);
sapling_commitments.extend(block_commitments.sapling.into_iter().map(Some));
#[cfg(feature = "orchard")]
orchard_commitments.extend(block_commitments.orchard.into_iter().map(Some));
#[cfg(feature = "orchard")]
ironwood_commitments.extend(block_commitments.ironwood.into_iter().map(Some));
}
#[cfg(feature = "transparent-inputs")]
for (account_id, key_scope) in wallet_db
.find_involved_accounts(tx_refs)
.map_err(PutBlocksError::Storage)?
{
if let Some(t_key_scope) = key_scope {
generate_transparent_gap_addresses(
wallet_db,
gap_limits,
account_id,
t_key_scope,
UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require),
)
.map_err(PutBlocksError::GapAddresses)?;
}
}
wallet_db
.prune_tracked_nullifiers(PRUNING_DEPTH)
.map_err(PutBlocksError::Storage)?;
Ok(PutBlocksRows {
sapling_commitments,
#[cfg(feature = "orchard")]
orchard_commitments,
#[cfg(feature = "orchard")]
ironwood_commitments,
note_positions,
last_scanned_height,
})
}
pub fn put_blocks<DbT, SE, TE>(
wallet_db: &mut DbT,
#[cfg(feature = "transparent-inputs")] gap_limits: GapLimits,
from_state: &ChainState,
blocks: Vec<ScannedBlock<<DbT as LowLevelWalletRead>::AccountId>>,
anchor_retention: Option<&AnchorRetention>,
) -> Result<(), PutBlocksError<SE, TE>>
where
DbT: PutBlocksDbT<SE, TE, <DbT as LowLevelWalletRead>::AccountRef>,
DbT::TxRef: Eq + Hash,
{
let rows = put_blocks_rows(
wallet_db,
#[cfg(feature = "transparent-inputs")]
gap_limits,
from_state,
blocks,
)?;
let mut sapling_commitments = rows.sapling_commitments;
#[cfg(feature = "orchard")]
let mut orchard_commitments = rows.orchard_commitments;
#[cfg(feature = "orchard")]
let mut ironwood_commitments = rows.ironwood_commitments;
let note_positions = rows.note_positions;
let last_scanned_height = rows.last_scanned_height;
if let Some(last_scanned_height) = last_scanned_height {
const CHUNK_SIZE: usize = 1024;
let sapling_subtrees = build_subtrees::<_, SAPLING_SHARD_HEIGHT>(
Position::from(from_state.final_sapling_tree().tree_size()),
&mut sapling_commitments,
CHUNK_SIZE,
);
#[cfg(feature = "orchard")]
let orchard_subtrees = build_subtrees::<_, ORCHARD_SHARD_HEIGHT>(
Position::from(from_state.final_orchard_tree().tree_size()),
&mut orchard_commitments,
CHUNK_SIZE,
);
#[cfg(feature = "orchard")]
let ironwood_subtrees = build_subtrees::<_, ORCHARD_SHARD_HEIGHT>(
Position::from(from_state.final_ironwood_tree().tree_size()),
&mut ironwood_commitments,
CHUNK_SIZE,
);
#[cfg(feature = "orchard")]
let (
missing_sapling_checkpoints,
missing_orchard_checkpoints,
missing_ironwood_checkpoints,
) = {
let sapling_checkpoint_positions = checkpoint_positions(&sapling_subtrees);
let orchard_checkpoint_positions = checkpoint_positions(&orchard_subtrees);
let ironwood_checkpoint_positions = checkpoint_positions(&ironwood_subtrees);
let [ensure_sapling, ensure_orchard, ensure_ironwood] = batch_ensure_heights(
&sapling_checkpoint_positions.keys().copied().collect(),
&orchard_checkpoint_positions.keys().copied().collect(),
&ironwood_checkpoint_positions.keys().copied().collect(),
anchor_retention,
from_state.block_height() + 1..=last_scanned_height,
);
(
ensure_checkpoints(
ensure_sapling.iter(),
&sapling_checkpoint_positions,
from_state.final_sapling_tree(),
),
ensure_checkpoints(
ensure_orchard.iter(),
&orchard_checkpoint_positions,
from_state.final_orchard_tree(),
),
ensure_checkpoints(
ensure_ironwood.iter(),
&ironwood_checkpoint_positions,
from_state.final_ironwood_tree(),
),
)
};
{
let mut sapling_subtrees = sapling_subtrees.into_iter();
#[cfg(feature = "orchard")]
let mut missing_checkpoints = missing_sapling_checkpoints.into_iter();
wallet_db.with_sapling_tree_mut(|sapling_tree| {
update_tree(
"Sapling",
from_state.final_sapling_tree(),
from_state.block_height(),
sapling_tree,
anchor_retention,
&mut sapling_subtrees,
#[cfg(feature = "orchard")]
&mut missing_checkpoints,
)
.map_err(|error| PutBlocksError::ShardTreeForBlockRange {
pool: ShieldedPool::Sapling,
block_range: from_state.block_height()..(last_scanned_height + 1),
error,
})
})?;
}
#[cfg(feature = "orchard")]
{
let mut orchard_subtrees = orchard_subtrees.into_iter();
let mut missing_checkpoints = missing_orchard_checkpoints.into_iter();
wallet_db.with_orchard_tree_mut(|orchard_tree| {
update_tree(
"Orchard",
from_state.final_orchard_tree(),
from_state.block_height(),
orchard_tree,
anchor_retention,
&mut orchard_subtrees,
&mut missing_checkpoints,
)
.map_err(|error| PutBlocksError::ShardTreeForBlockRange {
pool: ShieldedPool::Orchard,
block_range: from_state.block_height()..(last_scanned_height + 1),
error,
})
})?;
}
#[cfg(feature = "orchard")]
{
let mut ironwood_subtrees = ironwood_subtrees.into_iter();
let mut missing_checkpoints = missing_ironwood_checkpoints.into_iter();
wallet_db.with_ironwood_tree_mut(|ironwood_tree| {
update_tree(
"Ironwood",
from_state.final_ironwood_tree(),
from_state.block_height(),
ironwood_tree,
anchor_retention,
&mut ironwood_subtrees,
&mut missing_checkpoints,
)
.map_err(|error| PutBlocksError::ShardTreeForBlockRange {
pool: ShieldedPool::Ironwood,
block_range: from_state.block_height()..(last_scanned_height + 1),
error,
})
})?;
}
wallet_db
.notify_scan_complete(
Range {
start: from_state.block_height() + 1,
end: last_scanned_height + 1,
},
¬e_positions,
)
.map_err(PutBlocksError::Storage)?;
}
Ok(())
}
#[cfg(not(feature = "transparent-inputs"))]
type GapError<DbT> = <DbT as LowLevelWalletRead>::Error;
#[cfg(not(feature = "transparent-inputs"))]
pub trait StoreDecryptedTxDbT: LowLevelWalletWrite {}
#[cfg(not(feature = "transparent-inputs"))]
impl<T: LowLevelWalletWrite> StoreDecryptedTxDbT for T {}
#[cfg(feature = "transparent-inputs")]
type GapError<DbT> = GapAddressesError<<DbT as LowLevelWalletRead>::Error>;
#[cfg(feature = "transparent-inputs")]
pub trait StoreDecryptedTxDbT:
LowLevelWalletWrite
+ AddressStore<
Error = <Self as LowLevelWalletRead>::Error,
AccountRef = <Self as LowLevelWalletRead>::AccountRef,
>
where
<Self as LowLevelWalletRead>::Error: From<GapError<Self>>,
{
}
#[cfg(feature = "transparent-inputs")]
impl<
T: LowLevelWalletWrite
+ AddressStore<
Error = <T as LowLevelWalletRead>::Error,
AccountRef = <T as LowLevelWalletRead>::AccountRef,
>,
> StoreDecryptedTxDbT for T
where
<T as LowLevelWalletRead>::Error: From<GapError<T>>,
{
}
pub fn store_decrypted_tx<DbT, P>(
wallet_db: &mut DbT,
params: &P,
#[cfg(feature = "transparent-inputs")] gap_limits: GapLimits,
chain_tip_height: BlockHeight,
d_tx: DecryptedTransaction<Transaction, <DbT as LowLevelWalletRead>::AccountId>,
) -> Result<(), <DbT as LowLevelWalletRead>::Error>
where
DbT: StoreDecryptedTxDbT,
<DbT as LowLevelWalletRead>::AccountId: core::fmt::Debug,
<DbT as LowLevelWalletRead>::Error: From<BalanceError> + From<GapError<DbT>>,
P: consensus::Parameters,
{
let funding_accounts = wallet_db.get_funding_accounts(d_tx.tx())?;
let funding_account = funding_accounts.iter().next().copied();
if funding_accounts.len() > 1 {
warn!(
"More than one wallet account detected as funding transaction {:?}, selecting {:?}",
d_tx.tx().txid(),
funding_account.unwrap()
)
}
let wallet_transparent_outputs =
detect_wallet_transparent_outputs::<_, _, <DbT as LowLevelWalletRead>::Error>(
params,
d_tx.tx(),
d_tx.mined_height(),
funding_account,
#[cfg(feature = "transparent-inputs")]
|address| wallet_db.find_account_for_transparent_address(address),
)?;
if funding_account.is_none()
&& wallet_transparent_outputs.is_empty()
&& !d_tx.has_decrypted_outputs()
{
wallet_db.delete_retrieval_queue_entries(d_tx.tx().txid())?;
return Ok(());
}
info!("Storing decrypted transaction with id {}", d_tx.tx().txid());
let observed_height = d_tx.mined_height().unwrap_or(chain_tip_height + 1);
let fee = determine_fee(wallet_db, d_tx.tx())?;
let tx_ref = wallet_db.put_tx_data(d_tx.tx(), fee, None, None, observed_height)?;
if let Some(height) = d_tx.mined_height() {
wallet_db.set_transaction_status(d_tx.tx().txid(), TransactionStatus::Mined(height))?;
}
#[cfg(feature = "orchard")]
{
let params = PoolMigrationParams::from(AnchorRetentionInterval::default());
let classification = crate::data_api::zip318::classify_decrypted_tx(
d_tx.tx(),
d_tx.orchard_outputs(),
d_tx.ironwood_outputs(),
¶ms,
);
wallet_db.put_zip318_classification(tx_ref, classification)?;
}
let has_wallet_shielded_spend = mark_notes_spent(
wallet_db,
tx_ref,
#[cfg(feature = "transparent-inputs")]
d_tx.tx()
.transparent_bundle()
.iter()
.flat_map(|b| b.vin.iter())
.map(|txin| txin.prevout()),
d_tx.tx()
.sapling_bundle()
.iter()
.flat_map(|b| b.shielded_spends().iter())
.map(|spend| spend.nullifier()),
#[cfg(feature = "orchard")]
d_tx.tx()
.orchard_bundle()
.iter()
.flat_map(|b| b.actions().iter())
.map(|action| action.nullifier()),
#[cfg(feature = "orchard")]
d_tx.tx()
.ironwood_bundle()
.iter()
.flat_map(|b| b.actions().iter())
.map(|action| action.nullifier()),
)?;
#[cfg(feature = "transparent-inputs")]
let mut tx_has_wallet_outputs = false;
#[cfg(feature = "transparent-inputs")]
{
tx_has_wallet_outputs |= !d_tx.sapling_outputs().is_empty();
#[cfg(feature = "orchard")]
{
tx_has_wallet_outputs |= !d_tx.orchard_outputs().is_empty();
tx_has_wallet_outputs |= !d_tx.ironwood_outputs().is_empty();
}
tx_has_wallet_outputs |= !wallet_transparent_outputs.is_empty();
}
#[cfg(feature = "transparent-inputs")]
let mut gap_update_set = HashSet::new();
put_shielded_outputs(
wallet_db,
Some(params),
tx_ref,
funding_account,
d_tx.sapling_outputs(),
|_, _| Ok(None),
|wallet_db, output, tx_ref, spent_in| {
wallet_db.put_received_sapling_note(output, tx_ref, d_tx.mined_height(), spent_in)
},
|_account_id| {
#[cfg(feature = "transparent-inputs")]
gap_update_set.insert((_account_id, TransparentKeyScope::EXTERNAL));
},
)?;
#[cfg(feature = "orchard")]
put_shielded_outputs(
wallet_db,
Some(params),
tx_ref,
funding_account,
d_tx.orchard_outputs(),
|_, _| Ok(None),
|wallet_db, output, tx_ref, spent_in| {
wallet_db.put_received_orchard_note(output, tx_ref, d_tx.mined_height(), spent_in)
},
|_account_id| {
#[cfg(feature = "transparent-inputs")]
gap_update_set.insert((_account_id, TransparentKeyScope::EXTERNAL));
},
)?;
#[cfg(feature = "orchard")]
put_shielded_outputs(
wallet_db,
Some(params),
tx_ref,
funding_account,
d_tx.ironwood_outputs(),
|_, _| Ok(None),
|wallet_db, output, tx_ref, spent_in| {
wallet_db.put_received_ironwood_note(output, tx_ref, d_tx.mined_height(), spent_in)
},
|_account_id| {
#[cfg(feature = "transparent-inputs")]
gap_update_set.insert((_account_id, TransparentKeyScope::EXTERNAL));
},
)?;
put_transparent_outputs(
wallet_db,
params,
tx_ref,
&wallet_transparent_outputs,
#[cfg(feature = "transparent-inputs")]
|wallet_db, output| wallet_db.put_transparent_output(output, observed_height, false),
#[cfg(feature = "transparent-inputs")]
|account_id, t_key_scope| {
gap_update_set.insert((account_id, t_key_scope));
},
)?;
#[cfg(feature = "transparent-inputs")]
for (account_id, key_scope) in gap_update_set {
generate_transparent_gap_addresses(
wallet_db,
gap_limits,
account_id,
key_scope,
UnifiedAddressRequest::unsafe_custom(Allow, Allow, Require),
)?;
}
for (spending_tx_ref, spending_tx) in
wallet_db.get_txs_spending_transparent_outputs_of(tx_ref)?
{
if let Some(fee) = determine_fee(wallet_db, &spending_tx)? {
wallet_db.update_tx_fee(spending_tx_ref, fee)?;
}
}
#[cfg(feature = "transparent-inputs")]
if tx_has_wallet_outputs {
wallet_db.queue_transparent_input_retrieval(tx_ref, &d_tx)?
}
wallet_db.delete_retrieval_queue_entries(d_tx.tx().txid())?;
if d_tx.mined_height().is_none() && !(has_wallet_shielded_spend || d_tx.has_decrypted_outputs())
{
wallet_db.queue_tx_status(d_tx.tx().txid())?;
}
Ok(())
}
pub(crate) fn detect_wallet_transparent_outputs<P, AccountId, E>(
params: &P,
tx: &Transaction,
mined_height: Option<BlockHeight>,
funding_account: Option<AccountId>,
#[cfg(feature = "transparent-inputs")] find_account_for_address: impl Fn(
&TransparentAddress,
) -> Result<
Option<(AccountId, Option<TransparentKeyScope>)>,
E,
>,
) -> Result<Vec<WalletTransparentOutput<AccountId>>, E>
where
P: consensus::Parameters,
AccountId: Copy + core::fmt::Debug + std::hash::Hash + std::cmp::Eq,
{
let mut result = vec![];
for (output_index, txout) in tx
.transparent_bundle()
.iter()
.flat_map(|b| b.vout.iter())
.enumerate()
{
let script_kind = txout.script_kind();
if let Some(address) = script_kind
.as_ref()
.and_then(TransparentAddress::from_script_kind)
{
debug!(
"{:?} output {} has recipient {}",
tx.txid(),
output_index,
address.encode(params)
);
#[allow(unused_mut)]
let mut detected = false;
#[cfg(feature = "transparent-inputs")]
if let Some((account_uuid, key_scope)) = find_account_for_address(&address)? {
debug!(
"{:?} output {} belongs to account {:?}",
tx.txid(),
output_index,
account_uuid
);
result.push(
WalletTransparentOutput::from_parts(
OutPoint::new(tx.txid().into(), u32::try_from(output_index).unwrap()),
txout.clone(),
mined_height,
Some(account_uuid),
key_scope,
funding_account,
)
.expect("txout.recipient_address extraction previously checked"),
);
detected = true;
} else {
debug!(
"Address {} is not recognized as belonging to any of our accounts.",
address.encode(params)
);
}
if !detected {
if let Some(account_id) = funding_account {
result.push(
WalletTransparentOutput::from_parts(
OutPoint::new(tx.txid().into(), u32::try_from(output_index).unwrap()),
txout.clone(),
mined_height,
None,
None,
Some(account_id),
)
.expect("txout.recipient_address extraction previously checked"),
);
}
}
} else if let Some(script_kind) = script_kind {
if !matches!(script_kind, ScriptKind::NullData { .. }) {
warn!(
"Ignoring unsupported script kind '{}' for tx {} output {}",
script_kind.as_str(),
tx.txid(),
output_index
);
}
} else {
warn!(
"Unable to determine recipient address for tx {} output {}",
tx.txid(),
output_index
);
}
}
Ok(result)
}
fn mark_notes_spent<'a, DbT>(
wallet_db: &mut DbT,
tx_ref: <DbT as LowLevelWalletRead>::TxRef,
#[cfg(feature = "transparent-inputs")] transparent_prevouts: impl Iterator<
Item = &'a transparent::bundle::OutPoint,
>,
sapling_nfs: impl Iterator<Item = &'a sapling::Nullifier>,
#[cfg(feature = "orchard")] orchard_nfs: impl Iterator<Item = &'a orchard::note::Nullifier>,
#[cfg(feature = "orchard")] ironwood_nfs: impl Iterator<Item = &'a orchard::note::Nullifier>,
) -> Result<bool, <DbT as LowLevelWalletRead>::Error>
where
DbT: LowLevelWalletWrite,
{
let mut has_wallet_shielded_spend = false;
#[cfg(feature = "transparent-inputs")]
for outpoint in transparent_prevouts {
wallet_db.mark_transparent_utxo_spent(outpoint, tx_ref)?;
}
for nf in sapling_nfs {
has_wallet_shielded_spend |= wallet_db.mark_sapling_note_spent(nf, tx_ref)?;
}
#[cfg(feature = "orchard")]
for nf in orchard_nfs {
has_wallet_shielded_spend |= wallet_db.mark_orchard_note_spent(nf, tx_ref)?;
}
#[cfg(feature = "orchard")]
for nf in ironwood_nfs {
has_wallet_shielded_spend |= wallet_db.mark_ironwood_note_spent(nf, tx_ref)?;
}
Ok(has_wallet_shielded_spend)
}
#[allow(clippy::too_many_arguments)]
fn put_shielded_outputs<DbT, P, Output>(
wallet_db: &mut DbT,
params: Option<&P>,
tx_ref: <DbT as LowLevelWalletRead>::TxRef,
funding_account: Option<DbT::AccountId>,
outputs: &[Output],
detect_note_spent_in: impl Fn(
&mut DbT,
&Output,
) -> Result<
Option<<DbT as LowLevelWalletRead>::TxRef>,
<DbT as LowLevelWalletRead>::Error,
>,
put_received_note: impl Fn(
&mut DbT,
&Output,
<DbT as LowLevelWalletRead>::TxRef,
Option<<DbT as LowLevelWalletRead>::TxRef>,
) -> Result<(), <DbT as LowLevelWalletRead>::Error>,
mut on_external_account: impl FnMut(<DbT as LowLevelWalletRead>::AccountId),
) -> Result<(), <DbT as LowLevelWalletRead>::Error>
where
DbT: LowLevelWalletWrite,
P: consensus::Parameters,
Output: ReceivedShieldedOutput<AccountId = <DbT as LowLevelWalletRead>::AccountId>,
{
for output in outputs {
let sent_output = match output.transfer_type() {
TransferType::Outgoing => {
let note = output.to_wallet_note();
let recipient = Recipient::External {
recipient_address: external_address(
wallet_db,
params.expect("present when outgoing is possible (store_decrypted_tx)"),
output.account_id(),
note.receiver(),
)?,
output_pool: PoolType::Shielded(note.pool()),
};
Some((output.account_id(), recipient, note.value()))
}
TransferType::AccountInternal => {
let spent_in = detect_note_spent_in(wallet_db, output)?;
put_received_note(wallet_db, output, tx_ref, spent_in)?;
let note = output.to_wallet_note();
let value = note.value();
let recipient = Recipient::InternalShielded {
receiving_account: output.account_id(),
external_address: None,
note: Box::new(note),
};
Some((output.account_id(), recipient, value))
}
TransferType::Incoming => {
let spent_in = detect_note_spent_in(wallet_db, output)?;
put_received_note(wallet_db, output, tx_ref, spent_in)?;
on_external_account(output.account_id());
if let Some(account_id) = funding_account {
let note = output.to_wallet_note();
let value = note.value();
let recipient = Recipient::InternalShielded {
receiving_account: output.account_id(),
external_address: Some(external_address(
wallet_db,
params.expect(
"present when funding_account is known (store_decrypted_tx)",
),
output.account_id(),
note.receiver(),
)?),
note: Box::new(note),
};
Some((account_id, recipient, value))
} else {
None
}
}
TransferType::WalletInternal => unreachable!(
"TransferType::WalletInternal is only produced for transparent outputs"
),
};
if let Some((from_account_uuid, recipient, value)) = sent_output {
wallet_db.put_sent_output(
from_account_uuid,
tx_ref,
output.index(),
&recipient,
value,
output.memo(),
)?;
}
}
Ok(())
}
fn put_transparent_outputs<DbT, P>(
wallet_db: &mut DbT,
params: &P,
tx_ref: <DbT as LowLevelWalletRead>::TxRef,
outputs: &[WalletTransparentOutput<<DbT as LowLevelWalletRead>::AccountId>],
#[cfg(feature = "transparent-inputs")] put_received_output: impl Fn(
&mut DbT,
&WalletTransparentOutput<<DbT as LowLevelWalletRead>::AccountId>,
) -> Result<
(
<DbT as LowLevelWalletRead>::AccountId,
std::option::Option<TransparentKeyScope>,
),
<DbT as LowLevelWalletRead>::Error,
>,
#[cfg(feature = "transparent-inputs")] mut on_received: impl FnMut(
<DbT as LowLevelWalletRead>::AccountId,
TransparentKeyScope,
),
) -> Result<(), <DbT as LowLevelWalletRead>::Error>
where
DbT: LowLevelWalletWrite,
P: consensus::Parameters,
{
for output in outputs {
#[cfg(feature = "transparent-inputs")]
if output.recipient_account().is_some() {
let (account_id, _) = put_received_output(wallet_db, output)?;
if let Some(t_key_scope) = output.recipient_key_scope() {
on_received(account_id, t_key_scope);
}
wallet_db.queue_transparent_spend_detection(
*output.recipient_address(),
tx_ref,
output.outpoint().n(),
)?;
}
if let Some(&from_account) = output.funding_account() {
let recipient = match output.recipient_account() {
#[cfg(feature = "transparent-inputs")]
Some(&receiving_account) => Recipient::InternalTransparent {
receiving_account,
recipient_address: *output.recipient_address(),
},
#[cfg(not(feature = "transparent-inputs"))]
Some(_) => Recipient::External {
recipient_address: Receiver::Transparent(*output.recipient_address())
.to_zcash_address(params.network_type()),
output_pool: PoolType::TRANSPARENT,
},
None => {
let receiver = Receiver::Transparent(*output.recipient_address());
#[cfg(feature = "transparent-inputs")]
let recipient_address =
external_address(wallet_db, params, from_account, receiver)?;
#[cfg(not(feature = "transparent-inputs"))]
let recipient_address = receiver.to_zcash_address(params.network_type());
Recipient::External {
recipient_address,
output_pool: PoolType::TRANSPARENT,
}
}
};
wallet_db.put_sent_output(
from_account,
tx_ref,
output.index(),
&recipient,
output.value(),
None,
)?;
}
}
Ok(())
}
fn external_address<DbT, P>(
wallet_db: &DbT,
params: &P,
account_id: DbT::AccountId,
receiver: Receiver,
) -> Result<zcash_address::ZcashAddress, <DbT as LowLevelWalletRead>::Error>
where
DbT: LowLevelWalletRead,
P: consensus::Parameters,
{
let recipient_address = wallet_db
.select_receiving_address(account_id, &receiver)?
.unwrap_or_else(|| receiver.to_zcash_address(params.network_type()));
Ok(recipient_address)
}
pub fn build_subtrees<H, const SHARD_HEIGHT: u8>(
start_position: Position,
commitments: &mut [Option<(H, Retention<BlockHeight>)>],
chunk_size: usize,
) -> Vec<(LocatedPrunableTree<H>, BTreeMap<BlockHeight, Position>)>
where
H: Clone + PartialEq + Hashable + Send + Sync,
{
commitments
.par_chunks_mut(chunk_size)
.enumerate()
.filter_map(|(i, chunk)| {
let start = start_position + (i * chunk_size) as u64;
let end = start + chunk.len() as u64;
shardtree::LocatedTree::from_iter(
start..end,
SHARD_HEIGHT.into(),
chunk.iter_mut().map(|n| n.take().expect("always Some")),
)
})
.map(|res| (res.subtree, res.checkpoints))
.collect()
}
#[cfg(feature = "orchard")]
pub fn checkpoint_positions<H>(
subtrees: &[(LocatedPrunableTree<H>, BTreeMap<BlockHeight, Position>)],
) -> BTreeMap<BlockHeight, Position> {
subtrees
.iter()
.flat_map(|(_, checkpoints)| checkpoints.iter())
.map(|(k, v)| (*k, *v))
.collect()
}
#[cfg(feature = "orchard")]
pub fn ensure_checkpoints<'a, H, I: Iterator<Item = &'a BlockHeight>, const DEPTH: u8>(
ensure_heights: I,
existing_checkpoint_positions: &BTreeMap<BlockHeight, Position>,
state_final_tree: &Frontier<H, DEPTH>,
) -> Vec<(BlockHeight, Checkpoint)> {
ensure_heights
.flat_map(|ensure_height| {
existing_checkpoint_positions
.range::<BlockHeight, _>(..=*ensure_height)
.last()
.map_or_else(
|| {
Some((
*ensure_height,
state_final_tree
.value()
.map_or_else(Checkpoint::tree_empty, |t| {
Checkpoint::at_position(t.position())
}),
))
},
|(existing_checkpoint_height, position)| {
if *existing_checkpoint_height < *ensure_height {
Some((*ensure_height, Checkpoint::at_position(*position)))
} else {
None
}
},
)
.into_iter()
})
.collect::<Vec<_>>()
}
pub const NULLIFIER_MAP_RETENTION_BLOCKS: u32 = 100;
fn nullifier_tracking_floor(
fully_scanned: Option<BlockHeight>,
from_state_height: BlockHeight,
batch_end: Option<BlockHeight>,
) -> Option<BlockHeight> {
if fully_scanned == Some(from_state_height) {
batch_end.and_then(|last| {
let floor =
BlockHeight::from(u32::from(last).saturating_sub(NULLIFIER_MAP_RETENTION_BLOCKS));
(floor > from_state_height + 1).then_some(floor)
})
} else {
None
}
}
fn should_track_nullifiers(
nullifier_tracking_floor: Option<BlockHeight>,
block_height: BlockHeight,
) -> bool {
nullifier_tracking_floor.is_none_or(|floor| block_height >= floor)
}
fn should_retain_anchor(anchor_retention: Option<&AnchorRetention>, height: BlockHeight) -> bool {
anchor_retention.is_some_and(|retention| retention.retains(height))
}
fn retain_anchor_checkpoint<S, const DEPTH: u8, const SHARD_HEIGHT: u8>(
tree: &mut ShardTree<S, DEPTH, SHARD_HEIGHT>,
anchor_retention: Option<&AnchorRetention>,
height: BlockHeight,
) -> Result<(), ShardTreeError<S::Error>>
where
S: ShardStore<CheckpointId = BlockHeight>,
S::H: Clone + PartialEq + Hashable,
{
if should_retain_anchor(anchor_retention, height) {
tree.ensure_retained(height)?;
}
Ok(())
}
#[cfg(feature = "orchard")]
pub fn cross_pool_ensure_heights(
sapling: &BTreeSet<BlockHeight>,
orchard: &BTreeSet<BlockHeight>,
ironwood: &BTreeSet<BlockHeight>,
) -> [BTreeSet<BlockHeight>; 3] {
let union = |a: &BTreeSet<BlockHeight>, b: &BTreeSet<BlockHeight>| {
a.union(b).copied().collect::<BTreeSet<BlockHeight>>()
};
[
union(orchard, ironwood),
union(sapling, ironwood),
union(sapling, orchard),
]
}
#[cfg(feature = "orchard")]
pub fn batch_ensure_heights(
sapling: &BTreeSet<BlockHeight>,
orchard: &BTreeSet<BlockHeight>,
ironwood: &BTreeSet<BlockHeight>,
anchor_retention: Option<&AnchorRetention>,
range: std::ops::RangeInclusive<BlockHeight>,
) -> [BTreeSet<BlockHeight>; 3] {
let mut ensure = cross_pool_ensure_heights(sapling, orchard, ironwood);
if let Some(retention) = anchor_retention {
let retained = retention.retained_in_range(range);
for pool in ensure.iter_mut() {
pool.extend(retained.iter().copied());
}
}
ensure
}
pub fn update_tree<S, const DEPTH: u8, const SHARD_HEIGHT: u8>(
protocol: &'static str,
frontier: &Frontier<S::H, DEPTH>,
frontier_height: BlockHeight,
tree: &mut ShardTree<S, DEPTH, SHARD_HEIGHT>,
anchor_retention: Option<&AnchorRetention>,
subtrees: impl Iterator<Item = (LocatedPrunableTree<S::H>, BTreeMap<BlockHeight, Position>)>,
#[cfg(feature = "orchard")] missing_checkpoints: impl Iterator<Item = (BlockHeight, Checkpoint)>,
) -> Result<(), ShardTreeError<S::Error>>
where
S: ShardStore<CheckpointId = BlockHeight>,
S::H: Clone + PartialEq + Hashable,
{
debug!(
"{protocol} initial tree size at {frontier_height:?}: {:?}",
frontier.tree_size()
);
tree.insert_frontier(
frontier.clone(),
Retention::Checkpoint {
id: frontier_height,
marking: Marking::Reference,
},
)?;
retain_anchor_checkpoint(tree, anchor_retention, frontier_height)?;
for (subtree, checkpoints) in subtrees {
for height in checkpoints.keys() {
retain_anchor_checkpoint(tree, anchor_retention, *height)?;
}
tree.insert_tree(subtree, checkpoints)?;
}
#[cfg(feature = "orchard")]
{
let min_checkpoint_height = tree
.store()
.min_checkpoint_id()
.map_err(ShardTreeError::Storage)?
.expect("At least one checkpoint was inserted (by insert_frontier)");
for (height, checkpoint) in missing_checkpoints {
if height > min_checkpoint_height {
debug!(
"Adding missing {protocol} checkpoint for height: {:?}: {:?}",
height,
checkpoint.position()
);
tree.store_mut()
.add_checkpoint(height, checkpoint.clone())
.map_err(ShardTreeError::Storage)?;
retain_anchor_checkpoint(tree, anchor_retention, height)?;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
#[cfg(feature = "orchard")]
use {super::cross_pool_ensure_heights, std::collections::BTreeSet};
use core::num::NonZeroU32;
use proptest::prelude::*;
use zcash_protocol::consensus::BlockHeight;
#[cfg(feature = "orchard")]
use super::batch_ensure_heights;
use super::{
NULLIFIER_MAP_RETENTION_BLOCKS, nullifier_tracking_floor, should_retain_anchor,
should_track_nullifiers,
};
use crate::data_api::anchor_retention::{AnchorRetention, AnchorRetentionInterval};
#[test]
fn out_of_order_ranges_track_fully() {
let h = BlockHeight::from;
assert_eq!(
nullifier_tracking_floor(Some(h(1_000)), h(500_000), Some(h(510_000))),
None
);
assert_eq!(
nullifier_tracking_floor(None, h(500_000), Some(h(510_000))),
None
);
assert_eq!(
nullifier_tracking_floor(Some(h(600_000)), h(500_000), Some(h(510_000))),
None
);
}
#[test]
fn frontier_batches_retain_the_trailing_window() {
let from = BlockHeight::from(500_000);
let last = BlockHeight::from(510_000);
let floor =
nullifier_tracking_floor(Some(from), from, Some(last)).expect("frontier ⇒ floor");
assert_eq!(
u32::from(last) - u32::from(floor),
NULLIFIER_MAP_RETENTION_BLOCKS
);
let short = BlockHeight::from(500_000 + NULLIFIER_MAP_RETENTION_BLOCKS / 2);
assert_eq!(
nullifier_tracking_floor(Some(from), from, Some(short)),
None
);
assert_eq!(nullifier_tracking_floor(Some(from), from, None), None);
}
#[test]
fn nullifier_tracking_floor_gating() {
let floor = BlockHeight::from(1000);
assert!(should_track_nullifiers(None, BlockHeight::from(0)));
assert!(should_track_nullifiers(None, BlockHeight::from(999)));
assert!(should_track_nullifiers(
Some(floor),
BlockHeight::from(1000)
));
assert!(should_track_nullifiers(
Some(floor),
BlockHeight::from(1001)
));
assert!(!should_track_nullifiers(
Some(floor),
BlockHeight::from(999)
));
assert!(!should_track_nullifiers(Some(floor), BlockHeight::from(0)));
}
#[test]
fn anchor_retention_gating() {
for interval in [
AnchorRetentionInterval::ZIP_318,
AnchorRetentionInterval::custom(NonZeroU32::new(12).expect("nonzero")),
] {
let blocks = interval.block_count().get();
let floor = BlockHeight::from(4 * blocks);
let policy = AnchorRetention::new(floor, interval);
let retention = Some(&policy);
assert!(!should_retain_anchor(None, BlockHeight::from(8 * blocks)));
assert!(should_retain_anchor(
retention,
BlockHeight::from(4 * blocks)
));
assert!(should_retain_anchor(
retention,
BlockHeight::from(8 * blocks)
));
assert!(!should_retain_anchor(
retention,
BlockHeight::from(3 * blocks)
));
assert!(!should_retain_anchor(
retention,
BlockHeight::from(4 * blocks + 1)
));
assert!(!should_retain_anchor(
retention,
BlockHeight::from(5 * blocks - 1)
));
}
}
#[cfg(feature = "orchard")]
prop_compose! {
fn arb_heights()(
heights in proptest::collection::vec(0u32..100, 0..20),
) -> BTreeSet<BlockHeight> {
heights.into_iter().map(BlockHeight::from).collect()
}
}
#[cfg(feature = "orchard")]
fn union(a: &BTreeSet<BlockHeight>, b: &BTreeSet<BlockHeight>) -> BTreeSet<BlockHeight> {
a.union(b).copied().collect()
}
proptest! {
#[test]
#[cfg(feature = "orchard")]
fn ensure_heights_align_all_pools(
sapling in arb_heights(),
orchard in arb_heights(),
ironwood in arb_heights(),
) {
let [ensure_sapling, ensure_orchard, ensure_ironwood] =
cross_pool_ensure_heights(&sapling, &orchard, &ironwood);
let total = union(&union(&sapling, &orchard), &ironwood);
prop_assert_eq!(union(&sapling, &ensure_sapling), total.clone());
prop_assert_eq!(union(&orchard, &ensure_orchard), total.clone());
prop_assert_eq!(union(&ironwood, &ensure_ironwood), total);
prop_assert_eq!(ensure_sapling, union(&orchard, &ironwood));
prop_assert_eq!(ensure_orchard, union(&sapling, &ironwood));
prop_assert_eq!(ensure_ironwood, union(&sapling, &orchard));
}
#[test]
#[cfg(feature = "orchard")]
fn ensure_heights_degrade_to_two_pools(
sapling in arb_heights(),
orchard in arb_heights(),
) {
let [ensure_sapling, ensure_orchard, ensure_ironwood] =
cross_pool_ensure_heights(&sapling, &orchard, &BTreeSet::new());
prop_assert_eq!(ensure_sapling, orchard.clone());
prop_assert_eq!(ensure_orchard, sapling.clone());
prop_assert_eq!(ensure_ironwood, union(&sapling, &orchard));
}
}
#[cfg(feature = "orchard")]
#[test]
fn retained_boundary_on_a_commitment_free_block_is_ensured() {
let h = BlockHeight::from;
let retention = AnchorRetention::new(
h(1_000),
AnchorRetentionInterval::custom(NonZeroU32::new(12).unwrap()),
);
let (sap_cp, orch_cp, iw_cp) = (
BTreeSet::from([h(1_198)]),
BTreeSet::from([h(1_205)]),
BTreeSet::new(),
);
for heights in cross_pool_ensure_heights(&sap_cp, &orch_cp, &iw_cp) {
assert!(
!heights.contains(&h(1_200)),
"cross-pool alignment must not supply the boundary; the union is what does"
);
}
let [sapling, orchard, ironwood] = batch_ensure_heights(
&sap_cp,
&orch_cp,
&iw_cp,
Some(&retention),
h(1_150)..=h(1_250),
);
for (pool, heights) in [
("sapling", &sapling),
("orchard", &orchard),
("ironwood", &ironwood),
] {
assert!(
heights.contains(&h(1_200)),
"{pool} must ensure the retained boundary 1200, got {heights:?}"
);
}
}
#[cfg(feature = "orchard")]
#[test]
fn no_retention_policy_is_exactly_cross_pool() {
let h = BlockHeight::from;
let sapling = BTreeSet::from([h(100), h(140)]);
let orchard = BTreeSet::from([h(120)]);
let ironwood = BTreeSet::from([h(160)]);
assert_eq!(
batch_ensure_heights(&sapling, &orchard, &ironwood, None, h(1)..=h(1_000)),
cross_pool_ensure_heights(&sapling, &orchard, &ironwood)
);
}
}