pub(crate) fn add_to_non_finalized_chain_unique<'block, NullifierT>(
    chain_nullifiers: &mut HashSet<NullifierT>,
    shielded_data_nullifiers: impl IntoIterator<Item = &'block NullifierT>
) -> Result<(), ValidateContextError>
where NullifierT: DuplicateNullifierError + Copy + Debug + Eq + Hash + 'block,
Expand description

Reject double-spends of nullifers:

  • both within the same JoinSplit (sprout only),
  • from different JoinSplits, sapling::Spends or orchard::Actions in this Transaction’s shielded data, or
  • one from this shielded data, and another from:
    • a previous transaction in this Block, or
    • a previous block in this non-finalized Chain.

(Duplicate finalized nullifiers are rejected during service contextual validation, see no_duplicates_in_finalized_chain for details.)

§Consensus

A nullifier MUST NOT repeat either within a transaction, or across transactions in a valid blockchain. Sprout and Sapling and Orchard nullifiers are considered disjoint, even if they have the same bit pattern.

https://zips.z.cash/protocol/protocol.pdf#nullifierset

We comply with the “disjoint” rule by storing the nullifiers for each pool in separate sets (also with different types), so that even if different pools have nullifiers with same bit pattern, they won’t be considered the same when determining uniqueness. This is enforced by the callers of this function.