use std::{
collections::{BTreeMap, HashMap, HashSet},
convert::Infallible,
fmt,
hash::Hash,
num::{NonZeroU8, NonZeroU32, NonZeroU64},
};
#[cfg(feature = "pczt")]
use super::wallet::{create_pczt_from_proposal, extract_and_store_transaction_from_pczt};
use assert_matches::assert_matches;
use group::ff::Field;
use incrementalmerkletree::{Marking, Retention};
use nonempty::NonEmpty;
use rand_08::{CryptoRng as CryptoRng08, Rng as Rng08, RngCore as RngCore08, SeedableRng};
use rand_chacha::ChaChaRng;
use secrecy::{ExposeSecret, Secret, SecretVec};
use shardtree::{
ShardTree,
error::ShardTreeError,
store::{ShardStore as _, memory::MemoryShardStore},
};
use subtle::ConditionallySelectable;
#[cfg(feature = "transparent-inputs")]
use {
super::{
CoinbaseFilter, TransactionsInvolvingAddress, TransparentBalances,
wallet::{
input_selection::ShieldingSelector, propose_shielding, propose_shielding_coinbase,
shield_transparent_funds,
},
},
crate::wallet::TransparentAddressMetadata,
::transparent::address::TransparentAddress,
zcash_keys::keys::transparent::gap_limits::GapLimits,
};
use ::sapling::{
note_encryption::{SaplingDomain, sapling_note_encryption},
prover::mock::{MockOutputProver, MockSpendProver},
util::generate_random_rseed,
zip32::DiversifiableFullViewingKey,
};
use zcash_address::ZcashAddress;
use zcash_keys::{
address::{Address, UnifiedAddress},
keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
};
use zcash_note_encryption::Domain;
use zcash_primitives::{
block::BlockHash,
transaction::{Transaction, TxId, components::sapling::zip212_enforcement, fees::FeeRule},
};
#[cfg(feature = "pczt")]
use zcash_proofs::prover::LocalTxProver;
use zcash_protocol::{
ShieldedPool,
consensus::{self, BlockHeight, Network, NetworkUpgrade, Parameters as _},
local_consensus::LocalNetwork,
memo::{Memo, MemoBytes},
value::{ZatBalance, Zatoshis},
};
#[cfg(feature = "transparent-key-import")]
use zcash_script::script;
use zip32::DiversifierIndex;
use zip321::Payment;
#[cfg(feature = "orchard")]
use {
super::ORCHARD_SHARD_HEIGHT,
crate::proto::compact_formats::CompactOrchardAction,
::orchard::{
note::{ExtractedNoteCommitment, Note as OrchardNote, NoteVersion, RandomSeed, Rho},
note_encryption::{IronwoodDomain, IronwoodNoteEncryption},
tree::MerkleHashOrchard,
},
group::ff::PrimeField,
pasta_curves::pallas,
zcash_note_encryption::ShieldedOutput,
};
use super::{
Account, AccountBalance, AccountBirthday, AccountMeta, AccountPurpose, AccountSource,
AddressInfo, BlockMetadata, DecryptedTransaction, InputSource, NoteFilter, NullifierQuery,
ReceivedNotes, ReceivedTransactionOutput, SAPLING_SHARD_HEIGHT, ScannedBlock, SeedRelevance,
SentTransaction, TransactionDataRequest, TransactionStatus, WalletCommitmentTrees, WalletRead,
WalletSummary, WalletTest, WalletWrite, Zip32Derivation,
anchor_retention::AnchorRetentionInterval,
chain::{BlockSource, ChainState, CommitmentTreeRoot, ScanSummary, scan_cached_blocks},
error::Error,
scanning::{ScanPriority, ScanRange},
wallet::{
ConfirmationsPolicy, SpendingKeys, create_proposed_transactions,
input_selection::{
GreedyInputSelector, InputSelector, LockFilter, LockedInputPolicy, SpendPolicy,
},
propose_send_max_transfer, propose_standard_transfer_to_address, propose_transfer,
},
};
#[derive(Clone, Debug)]
pub struct TestRng(ChaChaRng);
impl TestRng {
pub fn seed_from_u64(seed: u64) -> Self {
Self(ChaChaRng::seed_from_u64(seed))
}
}
impl RngCore08 for TestRng {
fn next_u32(&mut self) -> u32 {
self.0.next_u32()
}
fn next_u64(&mut self) -> u64 {
self.0.next_u64()
}
fn fill_bytes(&mut self, dest: &mut [u8]) {
self.0.fill_bytes(dest);
}
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_08::Error> {
self.0.try_fill_bytes(dest)
}
}
impl CryptoRng08 for TestRng {}
impl rand::rand_core::TryRng for TestRng {
type Error = Infallible;
fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
Ok(self.0.next_u32())
}
fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
Ok(self.0.next_u64())
}
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Self::Error> {
self.0.fill_bytes(dest);
Ok(())
}
}
impl rand::rand_core::TryCryptoRng for TestRng {}
pub trait TestRngCore: RngCore08 + CryptoRng08 + rand::Rng + rand::rand_core::CryptoRng {}
impl<R> TestRngCore for R where R: RngCore08 + CryptoRng08 + rand::Rng + rand::rand_core::CryptoRng {}
pub fn random_frontier_with_prior_subtree_roots<H, R, const DEPTH: u8>(
rng: &mut R,
tree_size: u64,
subtree_depth: NonZeroU8,
mut random_node: impl FnMut(&mut R) -> H,
) -> (Vec<H>, incrementalmerkletree::frontier::Frontier<H, DEPTH>)
where
H: incrementalmerkletree::Hashable + Clone,
{
use incrementalmerkletree::{Level, Position, frontier::Frontier};
assert!(tree_size <= 2u64.checked_pow(DEPTH.into()).unwrap());
let Some(tree_size) = NonZeroU64::new(tree_size) else {
return (vec![], Frontier::empty());
};
let prior_subtree_count: u64 = u64::from(tree_size) >> u8::from(subtree_depth);
let prior_roots: Vec<H> = core::iter::repeat_with(|| random_node(rng))
.take(prior_subtree_count as usize)
.collect();
let position = Position::from(u64::from(tree_size) - 1);
let leaf = random_node(rng);
let mut ommers: Vec<H> = core::iter::repeat_with(|| random_node(rng))
.take(position.past_ommer_count().into())
.collect();
if !prior_roots.is_empty() {
let subtree_root_level = Level::from(u8::from(subtree_depth));
let mut replacement_ommers: Vec<(Level, H)> = vec![];
let mut roots_iter = prior_roots.iter();
loop {
if let Some(top) = replacement_ommers.pop() {
if let Some(prev) = replacement_ommers.pop() {
if top.0 == prev.0 {
replacement_ommers.push((top.0 + 1, H::combine(top.0, &prev.1, &top.1)));
continue;
} else {
replacement_ommers.push(prev);
}
}
if let Some(root) = roots_iter.next() {
if top.0 == subtree_root_level {
replacement_ommers.push((
subtree_root_level + 1,
H::combine(subtree_root_level, &top.1, root),
));
} else {
replacement_ommers.push(top);
replacement_ommers.push((subtree_root_level, root.clone()));
}
} else {
replacement_ommers.push(top);
break;
}
} else if let Some(root) = roots_iter.next() {
replacement_ommers.push((subtree_root_level, root.clone()));
} else {
break;
}
}
let ommer_count = ommers.len();
for (idx, (_, ommer)) in replacement_ommers.into_iter().enumerate() {
ommers[ommer_count - (idx + 1)] = ommer;
}
}
(
prior_roots,
Frontier::from_parts(position, leaf, ommers).expect("generated frontier is valid"),
)
}
#[cfg(feature = "pczt")]
fn real_test_prover() -> &'static LocalTxProver {
use std::sync::OnceLock;
static PROVER: OnceLock<LocalTxProver> = OnceLock::new();
PROVER.get_or_init(LocalTxProver::bundled)
}
use crate::{
data_api::{
MaxSpendMode, OutputLockStore, TargetValue,
error::{LockError, RewindError},
wallet::TargetHeight,
},
fees::{
ChangeStrategy, DustOutputPolicy, StandardFeeRule,
standard::{self, SingleOutputChangeStrategy},
},
proposal::Proposal,
proto::compact_formats::{
self, CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx,
},
wallet::{
LockOwner, Note, NoteId, OutputRef, OvkPolicy, ReceivedNote, WalletTransparentOutput,
},
};
pub mod pool;
pub mod sapling;
#[cfg(feature = "orchard")]
pub mod orchard;
#[cfg(feature = "transparent-inputs")]
pub mod transparent;
#[derive(Debug)]
pub struct TransactionSummary<AccountId> {
account_id: AccountId,
txid: TxId,
expiry_height: Option<BlockHeight>,
mined_height: Option<BlockHeight>,
account_value_delta: ZatBalance,
total_spent: Zatoshis,
total_received: Zatoshis,
fee_paid: Option<Zatoshis>,
spent_note_count: usize,
has_change: bool,
sent_note_count: usize,
received_note_count: usize,
memo_count: usize,
expired_unmined: bool,
is_shielding: bool,
pool_crossing_value: Option<Zatoshis>,
}
impl<AccountId> TransactionSummary<AccountId> {
#[allow(clippy::too_many_arguments)]
pub fn from_parts(
account_id: AccountId,
txid: TxId,
expiry_height: Option<BlockHeight>,
mined_height: Option<BlockHeight>,
account_value_delta: ZatBalance,
total_spent: Zatoshis,
total_received: Zatoshis,
fee_paid: Option<Zatoshis>,
spent_note_count: usize,
has_change: bool,
sent_note_count: usize,
received_note_count: usize,
memo_count: usize,
expired_unmined: bool,
is_shielding: bool,
pool_crossing_value: Option<Zatoshis>,
) -> Self {
Self {
account_id,
txid,
expiry_height,
mined_height,
account_value_delta,
total_spent,
total_received,
fee_paid,
spent_note_count,
has_change,
sent_note_count,
received_note_count,
memo_count,
expired_unmined,
is_shielding,
pool_crossing_value,
}
}
pub fn account_id(&self) -> &AccountId {
&self.account_id
}
pub fn txid(&self) -> TxId {
self.txid
}
pub fn expiry_height(&self) -> Option<BlockHeight> {
self.expiry_height
}
pub fn mined_height(&self) -> Option<BlockHeight> {
self.mined_height
}
pub fn account_value_delta(&self) -> ZatBalance {
self.account_value_delta
}
pub fn total_spent(&self) -> Zatoshis {
self.total_spent
}
pub fn total_received(&self) -> Zatoshis {
self.total_received
}
pub fn fee_paid(&self) -> Option<Zatoshis> {
self.fee_paid
}
pub fn spent_note_count(&self) -> usize {
self.spent_note_count
}
pub fn has_change(&self) -> bool {
self.has_change
}
pub fn sent_note_count(&self) -> usize {
self.sent_note_count
}
pub fn received_note_count(&self) -> usize {
self.received_note_count
}
pub fn expired_unmined(&self) -> bool {
self.expired_unmined
}
pub fn memo_count(&self) -> usize {
self.memo_count
}
pub fn is_shielding(&self) -> bool {
self.is_shielding
}
pub fn is_pool_crossing(&self) -> bool {
self.pool_crossing_value.is_some()
}
pub fn pool_crossing_value(&self) -> Option<Zatoshis> {
self.pool_crossing_value
}
}
#[derive(Clone, Debug)]
pub struct CachedBlock {
chain_state: ChainState,
sapling_end_size: u32,
orchard_end_size: u32,
ironwood_end_size: u32,
}
impl CachedBlock {
pub fn none(block_height: BlockHeight) -> Self {
Self {
chain_state: ChainState::empty(block_height, BlockHash([0; 32])),
sapling_end_size: 0,
orchard_end_size: 0,
ironwood_end_size: 0,
}
}
pub fn at(
chain_state: ChainState,
sapling_end_size: u32,
orchard_end_size: u32,
ironwood_end_size: u32,
) -> Self {
assert_eq!(
chain_state.final_sapling_tree().tree_size() as u32,
sapling_end_size
);
#[cfg(feature = "orchard")]
assert_eq!(
chain_state.final_orchard_tree().tree_size() as u32,
orchard_end_size
);
#[cfg(feature = "orchard")]
assert_eq!(
chain_state.final_ironwood_tree().tree_size() as u32,
ironwood_end_size
);
Self {
chain_state,
sapling_end_size,
orchard_end_size,
ironwood_end_size,
}
}
fn roll_forward(&self, cb: &CompactBlock) -> Self {
assert_eq!(self.chain_state.block_height() + 1, cb.height());
let sapling_final_tree = cb.vtx.iter().flat_map(|tx| tx.outputs.iter()).fold(
self.chain_state.final_sapling_tree().clone(),
|mut acc, c_out| {
acc.append(::sapling::Node::from_cmu(&c_out.cmu().unwrap()));
acc
},
);
let sapling_end_size = sapling_final_tree.tree_size() as u32;
#[cfg(feature = "orchard")]
let orchard_final_tree = cb.vtx.iter().flat_map(|tx| tx.actions.iter()).fold(
self.chain_state.final_orchard_tree().clone(),
|mut acc, c_act| {
acc.append(MerkleHashOrchard::from_cmx(&c_act.cmx().unwrap()));
acc
},
);
#[cfg(feature = "orchard")]
let orchard_end_size = orchard_final_tree.tree_size() as u32;
#[cfg(not(feature = "orchard"))]
let orchard_end_size = cb.vtx.iter().fold(self.orchard_end_size, |sz, tx| {
sz + (tx.actions.len() as u32)
});
#[cfg(feature = "orchard")]
let ironwood_final_tree = cb
.vtx
.iter()
.flat_map(|tx| tx.ironwood_actions.iter())
.fold(
self.chain_state.final_ironwood_tree().clone(),
|mut acc, c_act| {
acc.append(MerkleHashOrchard::from_cmx(&c_act.cmx().unwrap()));
acc
},
);
#[cfg(feature = "orchard")]
let ironwood_end_size = ironwood_final_tree.tree_size() as u32;
#[cfg(not(feature = "orchard"))]
let ironwood_end_size = cb.vtx.iter().fold(self.ironwood_end_size, |sz, tx| {
sz + (tx.ironwood_actions.len() as u32)
});
Self {
chain_state: ChainState::new(
cb.height(),
cb.hash(),
sapling_final_tree,
#[cfg(feature = "orchard")]
orchard_final_tree,
#[cfg(feature = "orchard")]
ironwood_final_tree,
),
sapling_end_size,
orchard_end_size,
ironwood_end_size,
}
}
pub fn chain_state(&self) -> &ChainState {
&self.chain_state
}
pub fn height(&self) -> BlockHeight {
self.chain_state.block_height()
}
pub fn sapling_end_size(&self) -> u32 {
self.sapling_end_size
}
pub fn orchard_end_size(&self) -> u32 {
self.orchard_end_size
}
pub fn ironwood_end_size(&self) -> u32 {
self.ironwood_end_size
}
}
#[derive(Clone)]
pub struct TestAccount<A> {
account: A,
usk: UnifiedSpendingKey,
birthday: AccountBirthday,
}
impl<A> TestAccount<A> {
pub fn account(&self) -> &A {
&self.account
}
pub fn usk(&self) -> &UnifiedSpendingKey {
&self.usk
}
pub fn birthday(&self) -> &AccountBirthday {
&self.birthday
}
}
impl<A: Account> Account for TestAccount<A> {
type AccountId = A::AccountId;
fn id(&self) -> Self::AccountId {
self.account.id()
}
fn name(&self) -> Option<&str> {
self.account.name()
}
fn birthday_height(&self) -> BlockHeight {
self.account.birthday_height()
}
fn source(&self) -> &AccountSource {
self.account.source()
}
fn ufvk(&self) -> Option<&zcash_keys::keys::UnifiedFullViewingKey> {
self.account.ufvk()
}
fn uivk(&self) -> zcash_keys::keys::UnifiedIncomingViewingKey {
self.account.uivk()
}
}
pub trait Reset: WalletTest + Sized {
type Handle;
fn reset<C>(st: &mut TestState<C, Self, LocalNetwork>) -> Self::Handle;
}
pub struct TestState<Cache, DataStore: WalletTest, Network> {
cache: Cache,
cached_blocks: BTreeMap<BlockHeight, CachedBlock>,
latest_block_height: Option<BlockHeight>,
wallet_data: DataStore,
network: Network,
test_account: Option<(SecretVec<u8>, TestAccount<DataStore::Account>)>,
rng: TestRng,
}
impl<Cache, DataStore: WalletTest, Network> TestState<Cache, DataStore, Network> {
pub fn wallet(&self) -> &DataStore {
&self.wallet_data
}
pub fn wallet_mut(&mut self) -> &mut DataStore {
&mut self.wallet_data
}
pub fn rng_mut(&mut self) -> &mut TestRng {
&mut self.rng
}
pub fn network(&self) -> &Network {
&self.network
}
}
impl<Cache, DataStore: WalletTest, Network> Drop for TestState<Cache, DataStore, Network> {
fn drop(&mut self) {
self.wallet_data.finally();
}
}
impl<Cache, DataStore: WalletTest, Network: consensus::Parameters>
TestState<Cache, DataStore, Network>
{
pub fn sapling_activation_height(&self) -> BlockHeight {
self.network
.activation_height(NetworkUpgrade::Sapling)
.expect("Sapling activation height must be known.")
}
#[allow(dead_code)]
pub fn nu5_activation_height(&self) -> BlockHeight {
self.network
.activation_height(NetworkUpgrade::Nu5)
.expect("NU5 activation height must be known.")
}
pub fn test_seed(&self) -> Option<&SecretVec<u8>> {
self.test_account.as_ref().map(|(seed, _)| seed)
}
pub fn test_account(&self) -> Option<&TestAccount<<DataStore as WalletRead>::Account>> {
self.test_account.as_ref().map(|(_, acct)| acct)
}
pub fn test_account_sapling(&self) -> Option<&DiversifiableFullViewingKey> {
let (_, acct) = self.test_account.as_ref()?;
let ufvk = acct.ufvk()?;
ufvk.sapling()
}
#[cfg(feature = "orchard")]
pub fn test_account_orchard(&self) -> Option<&::orchard::keys::FullViewingKey> {
let (_, acct) = self.test_account.as_ref()?;
let ufvk = acct.ufvk()?;
ufvk.orchard()
}
}
impl<Cache, DataStore, Network> TestState<Cache, DataStore, Network>
where
DataStore: WalletTest + WalletWrite,
Network: consensus::Parameters,
{
pub fn create_account_from_test_seed(
&mut self,
account_name: &str,
) -> (<DataStore as WalletRead>::AccountId, UnifiedSpendingKey) {
let seed = SecretVec::new(
self.test_seed()
.expect("the test state was built with a seed")
.expose_secret()
.clone(),
);
let birthday = self
.test_account()
.expect("the test state was built with an account")
.birthday()
.clone();
self.wallet_mut()
.create_account(account_name, &seed, &birthday, None)
.expect("creates a further account under the test seed")
}
}
impl<Cache: TestCache, DataStore, Network> TestState<Cache, DataStore, Network>
where
Network: consensus::Parameters,
DataStore: WalletTest + WalletWrite,
<Cache::BlockSource as BlockSource>::Error: fmt::Debug,
{
#[cfg(feature = "unstable")]
pub fn cache(&self) -> &Cache::BlockSource {
self.cache.block_source()
}
pub fn latest_cached_block(&self) -> Option<&CachedBlock> {
self.latest_block_height
.as_ref()
.and_then(|h| self.cached_blocks.get(h))
}
fn latest_cached_block_below_height(&self, height: BlockHeight) -> Option<&CachedBlock> {
self.cached_blocks.range(..height).last().map(|(_, b)| b)
}
fn cache_block(
&mut self,
prev_block: &CachedBlock,
compact_block: CompactBlock,
) -> Cache::InsertResult {
self.cached_blocks.insert(
compact_block.height(),
prev_block.roll_forward(&compact_block),
);
self.cache.insert(&compact_block)
}
pub fn generate_next_block<Fvk: TestFvk>(
&mut self,
recipient_fvk: &Fvk,
recipient_address_type: AddressType,
value: Zatoshis,
) -> (BlockHeight, Cache::InsertResult, Fvk::Nullifier) {
let (height, res, nfs) = self.generate_next_block_multi(&[FakeCompactOutput::new(
recipient_fvk,
recipient_address_type,
value,
)]);
(height, res, nfs[0])
}
pub fn generate_next_block_multi<Fvk: TestFvk>(
&mut self,
outputs: &[FakeCompactOutput<Fvk>],
) -> (BlockHeight, Cache::InsertResult, Vec<Fvk::Nullifier>) {
let pre_activation_block = CachedBlock::none(self.sapling_activation_height() - 1);
let prior_cached_block = self.latest_cached_block().unwrap_or(&pre_activation_block);
let height = prior_cached_block.height() + 1;
let (res, nfs) = self.generate_block_at(
height,
prior_cached_block.chain_state.block_hash(),
outputs,
prior_cached_block.sapling_end_size,
prior_cached_block.orchard_end_size,
prior_cached_block.ironwood_end_size,
false,
);
(height, res, nfs)
}
pub fn generate_empty_block(&mut self) -> (BlockHeight, Cache::InsertResult) {
let new_hash = {
let mut hash = vec![0; 32];
self.rng.fill_bytes(&mut hash);
hash
};
let pre_activation_block = CachedBlock::none(self.sapling_activation_height() - 1);
let prior_cached_block = self
.latest_cached_block()
.unwrap_or(&pre_activation_block)
.clone();
let new_height = prior_cached_block.height() + 1;
let mut cb = CompactBlock {
hash: new_hash,
height: new_height.into(),
..Default::default()
};
cb.prev_hash
.extend_from_slice(&prior_cached_block.chain_state.block_hash().0);
cb.chain_metadata = Some(compact_formats::ChainMetadata {
sapling_commitment_tree_size: prior_cached_block.sapling_end_size,
orchard_commitment_tree_size: prior_cached_block.orchard_end_size,
ironwood_commitment_tree_size: prior_cached_block.ironwood_end_size,
});
let res = self.cache_block(&prior_cached_block, cb);
self.latest_block_height = Some(new_height);
(new_height, res)
}
#[allow(clippy::too_many_arguments)]
pub fn generate_block_at<Fvk: TestFvk>(
&mut self,
height: BlockHeight,
prev_hash: BlockHash,
outputs: &[FakeCompactOutput<Fvk>],
initial_sapling_tree_size: u32,
initial_orchard_tree_size: u32,
initial_ironwood_tree_size: u32,
allow_broken_hash_chain: bool,
) -> (Cache::InsertResult, Vec<Fvk::Nullifier>) {
let mut prior_cached_block = self
.latest_cached_block_below_height(height)
.cloned()
.unwrap_or_else(|| CachedBlock::none(self.sapling_activation_height() - 1));
assert!(prior_cached_block.chain_state.block_height() < height);
assert!(prior_cached_block.sapling_end_size <= initial_sapling_tree_size);
assert!(prior_cached_block.orchard_end_size <= initial_orchard_tree_size);
assert!(prior_cached_block.ironwood_end_size <= initial_ironwood_tree_size);
if prior_cached_block.chain_state.block_height() == height - 1 {
if !allow_broken_hash_chain {
assert_eq!(prev_hash, prior_cached_block.chain_state.block_hash());
}
} else {
let final_sapling_tree =
(prior_cached_block.sapling_end_size..initial_sapling_tree_size).fold(
prior_cached_block.chain_state.final_sapling_tree().clone(),
|mut acc, _| {
acc.append(::sapling::Node::from_scalar(bls12_381::Scalar::random(
&mut self.rng,
)));
acc
},
);
#[cfg(feature = "orchard")]
let final_orchard_tree =
(prior_cached_block.orchard_end_size..initial_orchard_tree_size).fold(
prior_cached_block.chain_state.final_orchard_tree().clone(),
|mut acc, _| {
acc.append(MerkleHashOrchard::random(&mut self.rng));
acc
},
);
#[cfg(feature = "orchard")]
let final_ironwood_tree =
(prior_cached_block.ironwood_end_size..initial_ironwood_tree_size).fold(
prior_cached_block.chain_state.final_ironwood_tree().clone(),
|mut acc, _| {
acc.append(MerkleHashOrchard::random(&mut self.rng));
acc
},
);
prior_cached_block = CachedBlock::at(
ChainState::new(
height - 1,
prev_hash,
final_sapling_tree,
#[cfg(feature = "orchard")]
final_orchard_tree,
#[cfg(feature = "orchard")]
final_ironwood_tree,
),
initial_sapling_tree_size,
initial_orchard_tree_size,
initial_ironwood_tree_size,
);
self.cached_blocks
.insert(height - 1, prior_cached_block.clone());
}
let (cb, nfs) = fake_compact_block(
&self.network,
height,
prev_hash,
None,
outputs,
initial_sapling_tree_size,
initial_orchard_tree_size,
initial_ironwood_tree_size,
&mut self.rng,
);
assert_eq!(cb.height(), height);
let res = self.cache_block(&prior_cached_block, cb);
self.latest_block_height = Some(height);
(res, nfs)
}
pub fn generate_next_block_spending<Fvk: TestFvk>(
&mut self,
sender_fvk: &Fvk,
note: (Fvk::Nullifier, Zatoshis),
to: impl Into<Address>,
value: Zatoshis,
) -> (BlockHeight, Cache::InsertResult) {
let prior_cached_block = self
.latest_cached_block()
.cloned()
.unwrap_or_else(|| CachedBlock::none(self.sapling_activation_height() - 1));
let height = prior_cached_block.height() + 1;
let cb = fake_compact_block_spending(
&self.network,
height,
prior_cached_block.chain_state.block_hash(),
note,
sender_fvk,
to.into(),
value,
prior_cached_block.sapling_end_size,
prior_cached_block.orchard_end_size,
prior_cached_block.ironwood_end_size,
&mut self.rng,
);
assert_eq!(cb.height(), height);
let res = self.cache_block(&prior_cached_block, cb);
self.latest_block_height = Some(height);
(height, res)
}
pub fn generate_next_block_including(
&mut self,
txid: TxId,
) -> (BlockHeight, Cache::InsertResult) {
let tx = self
.wallet()
.get_transaction(txid)
.unwrap()
.expect("TxId should exist in the wallet");
self.generate_next_block_from_tx(1, &tx)
}
pub fn generate_next_block_from_tx(
&mut self,
tx_index: usize,
tx: &Transaction,
) -> (BlockHeight, Cache::InsertResult) {
let prior_cached_block = self
.latest_cached_block()
.cloned()
.unwrap_or_else(|| CachedBlock::none(self.sapling_activation_height() - 1));
let height = prior_cached_block.height() + 1;
let cb = fake_compact_block_from_tx(
height,
prior_cached_block.chain_state.block_hash(),
tx_index,
tx,
prior_cached_block.sapling_end_size,
prior_cached_block.orchard_end_size,
prior_cached_block.ironwood_end_size,
&mut self.rng,
);
assert_eq!(cb.height(), height);
let res = self.cache_block(&prior_cached_block, cb);
self.latest_block_height = Some(height);
(height, res)
}
pub fn truncate_to_height(&mut self, height: BlockHeight) {
self.wallet_mut().truncate_to_height(height).unwrap();
self.cache.truncate_to_height(height);
self.cached_blocks.split_off(&(height + 1));
self.latest_block_height = Some(height);
}
pub fn truncate_to_height_retaining_cache(&mut self, height: BlockHeight) {
self.wallet_mut().truncate_to_height(height).unwrap();
self.latest_block_height = Some(height);
}
}
impl<Cache, DbT, ParamsT> TestState<Cache, DbT, ParamsT>
where
Cache: TestCache,
<Cache::BlockSource as BlockSource>::Error: fmt::Debug,
ParamsT: consensus::Parameters + Send + 'static,
DbT: InputSource + WalletTest + WalletWrite + WalletCommitmentTrees,
<DbT as WalletRead>::AccountId:
std::fmt::Debug + ConditionallySelectable + Default + Send + Sync + 'static,
{
pub fn scan_cached_blocks(&mut self, from_height: BlockHeight, limit: usize) -> ScanSummary {
let result = self.try_scan_cached_blocks(from_height, limit);
assert_matches!(result, Ok(_));
result.unwrap()
}
pub fn try_scan_cached_blocks(
&mut self,
from_height: BlockHeight,
limit: usize,
) -> Result<
ScanSummary,
super::chain::error::Error<
<DbT as WalletRead>::Error,
<Cache::BlockSource as BlockSource>::Error,
>,
> {
let prior_cached_block = self
.latest_cached_block_below_height(from_height)
.cloned()
.unwrap_or_else(|| CachedBlock::none(from_height - 1));
scan_cached_blocks(
&self.network,
self.cache.block_source(),
&mut self.wallet_data,
from_height,
&prior_cached_block.chain_state,
limit,
)
}
pub fn try_scan_cached_blocks_with_state(
&mut self,
from_height: BlockHeight,
from_state: &ChainState,
limit: usize,
) -> Result<
ScanSummary,
super::chain::error::Error<
<DbT as WalletRead>::Error,
<Cache::BlockSource as BlockSource>::Error,
>,
> {
scan_cached_blocks(
&self.network,
self.cache.block_source(),
&mut self.wallet_data,
from_height,
from_state,
limit,
)
}
pub fn put_subtree_roots(
&mut self,
sapling_start_index: u64,
sapling_roots: &[CommitmentTreeRoot<::sapling::Node>],
#[cfg(feature = "orchard")] orchard_start_index: u64,
#[cfg(feature = "orchard")] orchard_roots: &[CommitmentTreeRoot<MerkleHashOrchard>],
) -> Result<(), ShardTreeError<<DbT as WalletCommitmentTrees>::Error>> {
self.wallet_mut()
.put_sapling_subtree_roots(sapling_start_index, sapling_roots)?;
#[cfg(feature = "orchard")]
self.wallet_mut()
.put_orchard_subtree_roots(orchard_start_index, orchard_roots)?;
Ok(())
}
pub fn generate_and_scan_empty_blocks(&mut self, n: usize) -> BlockHeight {
for _ in 0..n {
let (height, _) = self.generate_empty_block();
self.scan_cached_blocks(height, 1);
}
self.wallet()
.block_fully_scanned()
.expect("the wallet reports its fully-scanned block")
.expect("the wallet is fully scanned")
.block_height()
}
#[cfg(feature = "orchard")]
pub fn orchard_anchor_at(
&mut self,
height: BlockHeight,
) -> Result<Option<::orchard::Anchor>, ShardTreeError<<DbT as WalletCommitmentTrees>::Error>>
{
self.wallet_mut()
.with_orchard_tree_mut(|tree| tree.root_at_checkpoint_id(&height))
.map(|root| root.map(::orchard::Anchor::from))
}
}
impl<Cache, DbT, ParamsT, AccountIdT, ErrT> TestState<Cache, DbT, ParamsT>
where
ParamsT: consensus::Parameters + Send + 'static,
AccountIdT: std::fmt::Debug + std::cmp::Eq + std::hash::Hash,
ErrT: std::fmt::Debug,
DbT: InputSource<AccountId = AccountIdT, Error = ErrT>
+ WalletTest
+ WalletRead<AccountId = AccountIdT, Error = ErrT>
+ WalletWrite
+ WalletCommitmentTrees,
<DbT as WalletRead>::AccountId: ConditionallySelectable + Default + Send + 'static,
{
pub fn create_standard_transaction(
&mut self,
from_account: &TestAccount<DbT::Account>,
to: ZcashAddress,
value: Zatoshis,
) -> Result<
NonEmpty<TxId>,
super::wallet::TransferErrT<
DbT,
GreedyInputSelector<DbT>,
standard::MultiOutputChangeStrategy<DbT>,
>,
> {
let input_selector = GreedyInputSelector::new();
#[cfg(not(feature = "orchard"))]
let fallback_change_pool = ShieldedPool::Sapling;
#[cfg(feature = "orchard")]
let fallback_change_pool = ShieldedPool::Orchard;
let change_strategy =
single_output_change_strategy(StandardFeeRule::Zip317, None, fallback_change_pool);
let request =
zip321::TransactionRequest::new(vec![Payment::without_memo(to, value)]).unwrap();
self.spend(
&input_selector,
&change_strategy,
from_account.usk(),
request,
OvkPolicy::Sender,
ConfirmationsPolicy::MIN,
)
}
#[allow(clippy::type_complexity)]
pub fn spend<InputsT, ChangeT>(
&mut self,
input_selector: &InputsT,
change_strategy: &ChangeT,
usk: &UnifiedSpendingKey,
request: zip321::TransactionRequest,
ovk_policy: OvkPolicy,
confirmations_policy: ConfirmationsPolicy,
) -> Result<NonEmpty<TxId>, super::wallet::TransferErrT<DbT, InputsT, ChangeT>>
where
InputsT: InputSelector<InputSource = DbT>,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{
let network = self.network().clone();
let account = self
.wallet()
.get_account_for_ufvk(&usk.to_unified_full_viewing_key())
.map_err(Error::DataSource)?
.ok_or(Error::KeyNotRecognized)?;
let proposal = propose_transfer(
self.wallet_mut(),
&network,
account.id(),
input_selector,
change_strategy,
request,
confirmations_policy,
&SpendPolicy::default(),
None,
None,
)?;
create_proposed_transactions(
self.wallet_mut(),
&network,
&MockSpendProver,
&MockOutputProver,
&SpendingKeys::from_unified_spending_key(usk.clone()),
ovk_policy,
&proposal,
None,
)
}
#[allow(clippy::type_complexity)]
pub fn propose_transfer<InputsT, ChangeT>(
&mut self,
spend_from_account: <DbT as InputSource>::AccountId,
input_selector: &InputsT,
change_strategy: &ChangeT,
request: zip321::TransactionRequest,
confirmations_policy: ConfirmationsPolicy,
) -> Result<
Proposal<ChangeT::FeeRule, <DbT as InputSource>::NoteRef>,
super::wallet::ProposeTransferErrT<DbT, Infallible, InputsT, ChangeT>,
>
where
InputsT: InputSelector<InputSource = DbT>,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{
let network = self.network().clone();
propose_transfer::<_, _, _, _, Infallible>(
self.wallet_mut(),
&network,
spend_from_account,
input_selector,
change_strategy,
request,
confirmations_policy,
&SpendPolicy::default(),
None,
None,
)
}
#[allow(clippy::type_complexity)]
pub fn propose_transfer_with_policy<InputsT, ChangeT>(
&mut self,
spend_from_account: <DbT as InputSource>::AccountId,
input_selector: &InputsT,
change_strategy: &ChangeT,
request: zip321::TransactionRequest,
confirmations_policy: ConfirmationsPolicy,
spend_policy: &SpendPolicy,
) -> Result<
Proposal<ChangeT::FeeRule, <DbT as InputSource>::NoteRef>,
super::wallet::ProposeTransferErrT<DbT, Infallible, InputsT, ChangeT>,
>
where
InputsT: InputSelector<InputSource = DbT>,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{
let network = self.network().clone();
propose_transfer::<_, _, _, _, Infallible>(
self.wallet_mut(),
&network,
spend_from_account,
input_selector,
change_strategy,
request,
confirmations_policy,
spend_policy,
None,
None,
)
}
#[allow(clippy::type_complexity)]
pub fn propose_send_max_transfer<FeeRuleT>(
&mut self,
spend_from_account: <DbT as InputSource>::AccountId,
fee_rule: &FeeRuleT,
to: ZcashAddress,
memo: Option<MemoBytes>,
mode: MaxSpendMode,
confirmations_policy: ConfirmationsPolicy,
) -> Result<
Proposal<FeeRuleT, <DbT as InputSource>::NoteRef>,
super::wallet::ProposeSendMaxErrT<DbT, Infallible, FeeRuleT>,
>
where
FeeRuleT: FeeRule + Clone,
{
let network = self.network().clone();
propose_send_max_transfer::<_, _, _, Infallible>(
self.wallet_mut(),
&network,
spend_from_account,
&[ShieldedPool::Sapling, ShieldedPool::Orchard],
fee_rule,
to,
memo,
mode,
confirmations_policy,
&LockedInputPolicy::Exclude,
None,
)
}
#[allow(clippy::type_complexity)]
#[allow(clippy::too_many_arguments)]
pub fn propose_standard_transfer<CommitmentTreeErrT>(
&mut self,
spend_from_account: <DbT as InputSource>::AccountId,
fee_rule: StandardFeeRule,
confirmations_policy: ConfirmationsPolicy,
to: &Address,
amount: Zatoshis,
memo: Option<MemoBytes>,
change_memo: Option<MemoBytes>,
fallback_change_pool: ShieldedPool,
) -> Result<
Proposal<StandardFeeRule, <DbT as InputSource>::NoteRef>,
super::wallet::ProposeTransferErrT<
DbT,
CommitmentTreeErrT,
GreedyInputSelector<DbT>,
SingleOutputChangeStrategy<DbT>,
>,
> {
let network = self.network().clone();
let result = propose_standard_transfer_to_address::<_, _, CommitmentTreeErrT>(
self.wallet_mut(),
&network,
fee_rule,
spend_from_account,
confirmations_policy,
to,
amount,
memo,
change_memo,
fallback_change_pool,
None,
None,
);
if let Ok(proposal) = &result {
check_proposal_serialization_roundtrip(&network, self.wallet(), proposal);
}
result
}
#[cfg(feature = "transparent-inputs")]
#[allow(clippy::type_complexity)]
#[allow(clippy::too_many_arguments)]
#[allow(dead_code)]
pub fn propose_shielding<InputsT, ChangeT>(
&mut self,
input_selector: &InputsT,
change_strategy: &ChangeT,
shielding_threshold: Zatoshis,
from_addrs: &[TransparentAddress],
to_account: <InputsT::InputSource as InputSource>::AccountId,
confirmations_policy: ConfirmationsPolicy,
output_filter: CoinbaseFilter,
) -> Result<
Proposal<ChangeT::FeeRule, Infallible>,
super::wallet::ProposeShieldingErrT<DbT, Infallible, InputsT, ChangeT>,
>
where
InputsT: ShieldingSelector<InputSource = DbT>,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{
let network = self.network().clone();
propose_shielding::<_, _, _, _, Infallible>(
self.wallet_mut(),
&network,
input_selector,
change_strategy,
shielding_threshold,
from_addrs,
to_account,
confirmations_policy,
output_filter,
None,
)
}
#[cfg(feature = "transparent-inputs")]
#[allow(clippy::type_complexity)]
#[allow(clippy::too_many_arguments)]
#[allow(dead_code)]
pub fn propose_shielding_coinbase<InputsT, FeeRuleT>(
&mut self,
input_selector: &InputsT,
fee_rule: &FeeRuleT,
shielding_threshold: Zatoshis,
from_addrs: &[TransparentAddress],
to_address: zcash_address::ZcashAddress,
memo: Option<zcash_protocol::memo::MemoBytes>,
limit: Option<usize>,
) -> Result<
Proposal<FeeRuleT, Infallible>,
super::wallet::ProposeShieldingCoinbaseErrT<DbT, Infallible, InputsT, FeeRuleT>,
>
where
InputsT: ShieldingSelector<InputSource = DbT>,
FeeRuleT: zcash_primitives::transaction::fees::FeeRule + Clone,
{
let network = self.network().clone();
propose_shielding_coinbase::<_, _, _, _, Infallible>(
self.wallet_mut(),
&network,
input_selector,
fee_rule,
shielding_threshold,
from_addrs,
to_address,
memo,
limit,
None,
)
}
#[allow(clippy::type_complexity)]
pub fn create_proposed_transactions<InputsErrT, FeeRuleT, ChangeErrT, N>(
&mut self,
usk: &UnifiedSpendingKey,
ovk_policy: OvkPolicy,
proposal: &Proposal<FeeRuleT, N>,
) -> Result<NonEmpty<TxId>, super::wallet::CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, N>>
where
FeeRuleT: FeeRule,
{
let network = self.network().clone();
create_proposed_transactions(
self.wallet_mut(),
&network,
&MockSpendProver,
&MockOutputProver,
&SpendingKeys::from_unified_spending_key(usk.clone()),
ovk_policy,
proposal,
None,
)
}
#[cfg(feature = "pczt")]
#[allow(clippy::type_complexity)]
pub fn create_pczt_from_proposal<InputsErrT, FeeRuleT, ChangeErrT>(
&mut self,
spend_from_account: <DbT as InputSource>::AccountId,
ovk_policy: OvkPolicy,
proposal: &Proposal<FeeRuleT, <DbT as InputSource>::NoteRef>,
expiry_height: Option<BlockHeight>,
) -> Result<
pczt::Pczt,
super::wallet::CreateErrT<DbT, InputsErrT, FeeRuleT, ChangeErrT, DbT::NoteRef>,
>
where
<DbT as WalletRead>::AccountId: serde::Serialize,
FeeRuleT: FeeRule,
{
let network = self.network().clone();
create_pczt_from_proposal(
self.wallet_mut(),
&network,
spend_from_account,
ovk_policy,
proposal,
expiry_height,
::zcash_primitives::transaction::builder::BundlePadding::DEFAULT,
)
}
#[cfg(feature = "pczt")]
#[allow(clippy::type_complexity)]
pub fn extract_and_store_transaction_from_pczt(
&mut self,
pczt: pczt::Pczt,
) -> Result<TxId, super::wallet::ExtractErrT<DbT, DbT::NoteRef>>
where
<DbT as WalletRead>::AccountId: serde::de::DeserializeOwned,
{
let prover = real_test_prover();
let (spend_vk, output_vk) = prover.verifying_keys();
extract_and_store_transaction_from_pczt(
self.wallet_mut(),
pczt,
Some((&spend_vk, &output_vk)),
None,
)
}
#[cfg(feature = "transparent-inputs")]
#[allow(clippy::type_complexity)]
#[allow(clippy::too_many_arguments)]
pub fn shield_transparent_funds<InputsT, ChangeT>(
&mut self,
input_selector: &InputsT,
change_strategy: &ChangeT,
shielding_threshold: Zatoshis,
usk: &UnifiedSpendingKey,
from_addrs: &[TransparentAddress],
to_account: <DbT as InputSource>::AccountId,
confirmations_policy: ConfirmationsPolicy,
) -> Result<NonEmpty<TxId>, super::wallet::ShieldErrT<DbT, InputsT, ChangeT>>
where
InputsT: ShieldingSelector<InputSource = DbT>,
ChangeT: ChangeStrategy<MetaSource = DbT>,
{
let network = self.network().clone();
shield_transparent_funds(
self.wallet_mut(),
&network,
&MockSpendProver,
&MockOutputProver,
input_selector,
change_strategy,
shielding_threshold,
&SpendingKeys::from_unified_spending_key(usk.clone()),
from_addrs,
to_account,
confirmations_policy,
)
}
fn with_account_balance<T, F: FnOnce(&AccountBalance) -> T>(
&self,
account: AccountIdT,
confirmations_policy: ConfirmationsPolicy,
f: F,
) -> T {
let binding = self
.wallet()
.get_wallet_summary(confirmations_policy)
.unwrap()
.unwrap();
f(binding.account_balances().get(&account).unwrap())
}
pub fn get_total_balance(&self, account: AccountIdT) -> Zatoshis {
self.with_account_balance(account, ConfirmationsPolicy::MIN, |balance| balance.total())
}
pub fn get_locked_balance(&self, account: AccountIdT) -> Zatoshis {
self.with_account_balance(account, ConfirmationsPolicy::MIN, |balance| {
balance.locked_value()
})
}
pub fn get_spendable_balance(
&self,
account: AccountIdT,
confirmations_policy: ConfirmationsPolicy,
) -> Zatoshis {
self.with_account_balance(account, confirmations_policy, |balance| {
balance.spendable_value()
})
}
pub fn get_pending_shielded_balance(
&self,
account: AccountIdT,
confirmations_policy: ConfirmationsPolicy,
) -> Zatoshis {
self.with_account_balance(account, confirmations_policy, |balance| {
balance.value_pending_spendability() + balance.change_pending_confirmation()
})
.unwrap()
}
#[allow(dead_code)]
pub fn get_pending_change(
&self,
account: AccountIdT,
confirmations_policy: ConfirmationsPolicy,
) -> Zatoshis {
self.with_account_balance(account, confirmations_policy, |balance| {
balance.change_pending_confirmation()
})
}
pub fn get_wallet_summary(
&self,
confirmations_policy: ConfirmationsPolicy,
) -> Option<WalletSummary<AccountIdT>> {
self.wallet()
.get_wallet_summary(confirmations_policy)
.unwrap()
}
}
impl<Cache, DbT, ParamsT, AccountIdT, ErrT> TestState<Cache, DbT, ParamsT>
where
ParamsT: consensus::Parameters + Send + 'static,
AccountIdT: std::cmp::Eq + std::hash::Hash,
ErrT: std::fmt::Debug,
DbT: InputSource<AccountId = AccountIdT, Error = ErrT>
+ WalletTest
+ WalletRead<AccountId = AccountIdT, Error = ErrT>
+ WalletWrite
+ WalletCommitmentTrees,
<DbT as WalletRead>::AccountId: ConditionallySelectable + Default + Send + 'static,
{
#[allow(dead_code)]
pub fn get_tx_from_history(
&self,
txid: TxId,
) -> Result<Option<TransactionSummary<AccountIdT>>, ErrT> {
let history = self.wallet().get_tx_history()?;
Ok(history.into_iter().find(|tx| tx.txid() == txid))
}
}
impl<Cache, DbT: WalletRead + Reset> TestState<Cache, DbT, LocalNetwork> {
pub fn reset(&mut self) -> DbT::Handle {
self.latest_block_height = None;
self.test_account = None;
DbT::reset(self)
}
}
pub fn single_output_change_strategy<DbT: InputSource>(
fee_rule: StandardFeeRule,
change_memo: Option<&str>,
fallback_change_pool: ShieldedPool,
) -> standard::SingleOutputChangeStrategy<DbT> {
let change_memo = change_memo.map(|m| MemoBytes::from(m.parse::<Memo>().unwrap()));
standard::SingleOutputChangeStrategy::new(
fee_rule,
change_memo,
fallback_change_pool,
DustOutputPolicy::default(),
)
}
fn check_proposal_serialization_roundtrip<ParamsT: consensus::Parameters, DbT: InputSource>(
params: &ParamsT,
wallet_data: &DbT,
proposal: &Proposal<StandardFeeRule, DbT::NoteRef>,
) {
let proposal_proto = crate::proto::proposal::Proposal::from_standard_proposal(proposal);
let deserialized_proposal = proposal_proto.try_into_standard_proposal(params, wallet_data);
assert_matches!(deserialized_proposal, Ok(r) if &r == proposal);
}
pub struct InitialChainState {
pub chain_state: ChainState,
pub prior_sapling_roots: Vec<CommitmentTreeRoot<::sapling::Node>>,
#[cfg(feature = "orchard")]
pub prior_orchard_roots: Vec<CommitmentTreeRoot<MerkleHashOrchard>>,
}
pub trait DataStoreFactory {
type Error: core::fmt::Debug;
type AccountId: std::fmt::Debug
+ ConditionallySelectable
+ Default
+ Hash
+ Eq
+ Send
+ Sync
+ 'static;
type Account: Account<AccountId = Self::AccountId> + Clone;
type DsError: core::fmt::Debug;
type DataStore: InputSource<AccountId = Self::AccountId, Error = Self::DsError>
+ WalletRead<AccountId = Self::AccountId, Account = Self::Account, Error = Self::DsError>
+ WalletTest
+ WalletWrite
+ WalletCommitmentTrees;
fn new_data_store(
&self,
network: LocalNetwork,
anchor_retention_interval: Option<AnchorRetentionInterval>,
#[cfg(feature = "transparent-inputs")] gap_limits: Option<GapLimits>,
) -> Result<Self::DataStore, Self::Error>;
}
pub struct TestBuilder<Cache, DataStoreFactory> {
rng: TestRng,
network: LocalNetwork,
cache: Cache,
ds_factory: DataStoreFactory,
initial_chain_state: Option<InitialChainState>,
account_birthday: Option<AccountBirthday>,
account_index: Option<zip32::AccountId>,
anchor_retention_interval: Option<AnchorRetentionInterval>,
#[cfg(feature = "transparent-inputs")]
gap_limits: Option<GapLimits>,
}
impl TestBuilder<(), ()> {
pub const DEFAULT_NETWORK: LocalNetwork = LocalNetwork {
overwinter: Some(BlockHeight::from_u32(1)),
sapling: Some(BlockHeight::from_u32(100_000)),
blossom: Some(BlockHeight::from_u32(100_000)),
heartwood: Some(BlockHeight::from_u32(100_000)),
canopy: Some(BlockHeight::from_u32(100_000)),
nu5: Some(BlockHeight::from_u32(100_000)),
nu6: None,
nu6_1: None,
nu6_2: None,
nu6_3: None,
#[cfg(zcash_unstable = "nu7")]
nu7: None,
};
pub fn new() -> Self {
TestBuilder {
rng: TestRng::seed_from_u64(0),
network: Self::DEFAULT_NETWORK,
cache: (),
ds_factory: (),
initial_chain_state: None,
account_birthday: None,
account_index: None,
anchor_retention_interval: None,
#[cfg(feature = "transparent-inputs")]
gap_limits: None,
}
}
}
impl Default for TestBuilder<(), ()> {
fn default() -> Self {
Self::new()
}
}
impl<A> TestBuilder<(), A> {
pub fn with_block_cache<C>(self, cache: C) -> TestBuilder<C, A> {
TestBuilder {
rng: self.rng,
network: self.network,
cache,
ds_factory: self.ds_factory,
initial_chain_state: self.initial_chain_state,
account_birthday: self.account_birthday,
account_index: self.account_index,
anchor_retention_interval: self.anchor_retention_interval,
#[cfg(feature = "transparent-inputs")]
gap_limits: self.gap_limits,
}
}
}
impl<A> TestBuilder<A, ()> {
pub fn with_data_store_factory<DsFactory>(
self,
ds_factory: DsFactory,
) -> TestBuilder<A, DsFactory> {
TestBuilder {
rng: self.rng,
network: self.network,
cache: self.cache,
ds_factory,
initial_chain_state: self.initial_chain_state,
account_birthday: self.account_birthday,
account_index: self.account_index,
anchor_retention_interval: self.anchor_retention_interval,
#[cfg(feature = "transparent-inputs")]
gap_limits: self.gap_limits,
}
}
}
impl<A, B> TestBuilder<A, B> {
pub fn with_network(mut self, network: LocalNetwork) -> Self {
self.network = network;
self
}
pub fn with_anchor_retention_interval(mut self, interval: AnchorRetentionInterval) -> Self {
self.anchor_retention_interval = Some(interval);
self
}
#[cfg(feature = "transparent-inputs")]
pub fn with_gap_limits(self, gap_limits: GapLimits) -> TestBuilder<A, B> {
TestBuilder {
rng: self.rng,
network: self.network,
cache: self.cache,
ds_factory: self.ds_factory,
initial_chain_state: self.initial_chain_state,
account_birthday: self.account_birthday,
account_index: self.account_index,
anchor_retention_interval: self.anchor_retention_interval,
gap_limits: Some(gap_limits),
}
}
}
impl<Cache, DsFactory> TestBuilder<Cache, DsFactory> {
pub fn with_initial_chain_state(
mut self,
chain_state: impl FnOnce(&mut TestRng, &LocalNetwork) -> InitialChainState,
) -> Self {
assert!(self.initial_chain_state.is_none());
assert!(self.account_birthday.is_none());
self.initial_chain_state = Some(chain_state(&mut self.rng, &self.network));
self
}
pub fn with_account_from_sapling_activation(mut self, prev_hash: BlockHash) -> Self {
assert!(self.account_birthday.is_none());
self.account_birthday = Some(AccountBirthday::from_parts(
ChainState::empty(
self.network
.activation_height(NetworkUpgrade::Sapling)
.unwrap()
- 1,
prev_hash,
),
None,
));
self
}
pub fn with_account_having_current_birthday(mut self) -> Self {
assert!(self.account_birthday.is_none());
assert!(self.initial_chain_state.is_some());
self.account_birthday = Some(AccountBirthday::from_parts(
self.initial_chain_state
.as_ref()
.unwrap()
.chain_state
.clone(),
None,
));
self
}
pub fn set_account_index(mut self, index: zip32::AccountId) -> Self {
assert!(self.account_index.is_none());
self.account_index = Some(index);
self
}
}
impl<Cache, DsFactory: DataStoreFactory> TestBuilder<Cache, DsFactory> {
pub fn build(self) -> TestState<Cache, DsFactory::DataStore, LocalNetwork> {
let mut cached_blocks = BTreeMap::new();
let mut wallet_data = self
.ds_factory
.new_data_store(
self.network,
self.anchor_retention_interval,
#[cfg(feature = "transparent-inputs")]
self.gap_limits,
)
.unwrap();
if let Some(initial_state) = &self.initial_chain_state {
wallet_data
.put_sapling_subtree_roots(0, &initial_state.prior_sapling_roots)
.unwrap();
wallet_data
.with_sapling_tree_mut(|t| {
t.insert_frontier(
initial_state.chain_state.final_sapling_tree().clone(),
Retention::Checkpoint {
id: initial_state.chain_state.block_height(),
marking: Marking::Reference,
},
)
})
.unwrap();
#[cfg(feature = "orchard")]
{
wallet_data
.put_orchard_subtree_roots(0, &initial_state.prior_orchard_roots)
.unwrap();
wallet_data
.with_orchard_tree_mut(|t| {
t.insert_frontier(
initial_state.chain_state.final_orchard_tree().clone(),
Retention::Checkpoint {
id: initial_state.chain_state.block_height(),
marking: Marking::Reference,
},
)
})
.unwrap();
}
let final_sapling_tree_size =
initial_state.chain_state.final_sapling_tree().tree_size() as u32;
let _final_orchard_tree_size = 0;
#[cfg(feature = "orchard")]
let _final_orchard_tree_size =
initial_state.chain_state.final_orchard_tree().tree_size() as u32;
let _final_ironwood_tree_size = 0;
#[cfg(feature = "orchard")]
let _final_ironwood_tree_size =
initial_state.chain_state.final_ironwood_tree().tree_size() as u32;
cached_blocks.insert(
initial_state.chain_state.block_height(),
CachedBlock {
chain_state: initial_state.chain_state.clone(),
sapling_end_size: final_sapling_tree_size,
orchard_end_size: _final_orchard_tree_size,
ironwood_end_size: _final_ironwood_tree_size,
},
);
};
let test_account = self.account_birthday.map(|birthday| {
let seed = Secret::new(vec![0u8; 32]);
let (account, usk) = match self.account_index {
Some(index) => wallet_data
.import_account_hd("", &seed, index, &birthday, None)
.expect("test account import succeeds"),
None => {
let result = wallet_data
.create_account("", &seed, &birthday, None)
.expect("test account creation succeeds");
(
wallet_data
.get_account(result.0)
.expect("retrieval of just-created account succeeds")
.expect("an account was created"),
result.1,
)
}
};
(
seed,
TestAccount {
account,
usk,
birthday,
},
)
});
TestState {
cache: self.cache,
cached_blocks,
latest_block_height: self
.initial_chain_state
.map(|s| s.chain_state.block_height()),
wallet_data,
network: self.network,
test_account,
rng: self.rng,
}
}
}
pub trait TestFvk: Clone {
type Nullifier: Copy;
type OutgoingViewingKey;
fn to_ovk(&self, scope: zip32::Scope) -> Self::OutgoingViewingKey;
fn ovk_bytes(&self, scope: zip32::Scope) -> [u8; 32];
fn add_spend<R: TestRngCore>(&self, ctx: &mut CompactTx, nf: Self::Nullifier, rng: &mut R);
#[allow(clippy::too_many_arguments)]
fn add_output<P: consensus::Parameters, R: TestRngCore>(
&self,
ctx: &mut CompactTx,
params: &P,
height: BlockHeight,
sender_ovk: Option<&Self::OutgoingViewingKey>,
recipient_address_type: AddressType,
value: Zatoshis,
initial_sapling_tree_size: u32,
rng: &mut R,
) -> Self::Nullifier;
#[allow(clippy::too_many_arguments)]
fn add_logical_action<P: consensus::Parameters, R: TestRngCore>(
&self,
ctx: &mut CompactTx,
params: &P,
height: BlockHeight,
nf_to_reveal_in_spend: Self::Nullifier,
sender_ovk: Option<&Self::OutgoingViewingKey>,
recipient_address_type: AddressType,
value: Zatoshis,
initial_sapling_tree_size: u32,
rng: &mut R,
) -> Self::Nullifier;
}
impl<A: TestFvk> TestFvk for &A {
type Nullifier = A::Nullifier;
type OutgoingViewingKey = A::OutgoingViewingKey;
fn to_ovk(&self, scope: zip32::Scope) -> Self::OutgoingViewingKey {
(*self).to_ovk(scope)
}
fn ovk_bytes(&self, scope: zip32::Scope) -> [u8; 32] {
(*self).ovk_bytes(scope)
}
fn add_spend<R: TestRngCore>(&self, ctx: &mut CompactTx, nf: Self::Nullifier, rng: &mut R) {
(*self).add_spend(ctx, nf, rng)
}
fn add_output<P: consensus::Parameters, R: TestRngCore>(
&self,
ctx: &mut CompactTx,
params: &P,
height: BlockHeight,
sender_ovk: Option<&Self::OutgoingViewingKey>,
recipient_address_type: AddressType,
value: Zatoshis,
initial_sapling_tree_size: u32,
rng: &mut R,
) -> Self::Nullifier {
(*self).add_output(
ctx,
params,
height,
sender_ovk,
recipient_address_type,
value,
initial_sapling_tree_size,
rng,
)
}
fn add_logical_action<P: consensus::Parameters, R: TestRngCore>(
&self,
ctx: &mut CompactTx,
params: &P,
height: BlockHeight,
nf_to_reveal_in_spend: Self::Nullifier,
sender_ovk: Option<&Self::OutgoingViewingKey>,
recipient_address_type: AddressType,
value: Zatoshis,
initial_sapling_tree_size: u32,
rng: &mut R,
) -> Self::Nullifier {
(*self).add_logical_action(
ctx,
params,
height,
nf_to_reveal_in_spend,
sender_ovk,
recipient_address_type,
value,
initial_sapling_tree_size,
rng,
)
}
}
impl TestFvk for DiversifiableFullViewingKey {
type Nullifier = ::sapling::Nullifier;
type OutgoingViewingKey = ::sapling::keys::OutgoingViewingKey;
fn to_ovk(&self, scope: zip32::Scope) -> Self::OutgoingViewingKey {
self.to_ovk(scope)
}
fn ovk_bytes(&self, scope: zip32::Scope) -> [u8; 32] {
self.to_ovk(scope).0
}
fn add_spend<R: TestRngCore>(&self, ctx: &mut CompactTx, nf: Self::Nullifier, _: &mut R) {
let cspend = CompactSaplingSpend { nf: nf.to_vec() };
ctx.spends.push(cspend);
}
fn add_output<P: consensus::Parameters, R: TestRngCore>(
&self,
ctx: &mut CompactTx,
params: &P,
height: BlockHeight,
sender_ovk: Option<&Self::OutgoingViewingKey>,
recipient_address_type: AddressType,
value: Zatoshis,
initial_sapling_tree_size: u32,
rng: &mut R,
) -> Self::Nullifier {
let recipient = match recipient_address_type {
AddressType::DefaultExternal => self.default_address().1,
AddressType::DiversifiedExternal(idx) => self.find_address(idx).unwrap().1,
AddressType::Internal => self.change_address().1,
};
let position = initial_sapling_tree_size + ctx.outputs.len() as u32;
let (cout, note) =
compact_sapling_output(params, height, recipient, value, sender_ovk.copied(), rng);
ctx.outputs.push(cout);
note.nf(&self.fvk().vk.nk, u64::from(position))
}
#[allow(clippy::too_many_arguments)]
fn add_logical_action<P: consensus::Parameters, R: TestRngCore>(
&self,
ctx: &mut CompactTx,
params: &P,
height: BlockHeight,
nf_to_reveal_in_spend: Self::Nullifier,
sender_ovk: Option<&Self::OutgoingViewingKey>,
recipient_address_type: AddressType,
value: Zatoshis,
initial_sapling_tree_size: u32,
rng: &mut R,
) -> Self::Nullifier {
self.add_spend(ctx, nf_to_reveal_in_spend, rng);
self.add_output(
ctx,
params,
height,
sender_ovk,
recipient_address_type,
value,
initial_sapling_tree_size,
rng,
)
}
}
#[cfg(feature = "orchard")]
impl TestFvk for ::orchard::keys::FullViewingKey {
type Nullifier = ::orchard::note::Nullifier;
type OutgoingViewingKey = ::orchard::keys::OutgoingViewingKey;
fn to_ovk(&self, scope: zip32::Scope) -> Self::OutgoingViewingKey {
self.to_ovk(scope)
}
fn ovk_bytes(&self, scope: zip32::Scope) -> [u8; 32] {
*self.to_ovk(scope).as_ref()
}
fn add_spend<R: TestRngCore>(
&self,
ctx: &mut CompactTx,
nullifier_to_reveal: Self::Nullifier,
rng: &mut R,
) {
let recipient = loop {
let mut bytes = [0; 32];
RngCore08::fill_bytes(rng, &mut bytes);
let sk = ::orchard::keys::SpendingKey::from_bytes(bytes);
if sk.is_some().into() {
break ::orchard::keys::FullViewingKey::from(&sk.unwrap())
.address_at(0u32, zip32::Scope::External);
}
};
let (cact, _) =
compact_orchard_action(nullifier_to_reveal, recipient, Zatoshis::ZERO, None, rng);
ctx.actions.push(cact);
}
fn add_output<P: consensus::Parameters, R: TestRngCore>(
&self,
ctx: &mut CompactTx,
_: &P,
_: BlockHeight,
sender_ovk: Option<&Self::OutgoingViewingKey>,
recipient_address_type: AddressType,
value: Zatoshis,
_: u32, mut rng: &mut R,
) -> Self::Nullifier {
let nullifier_to_reveal =
::orchard::note::Nullifier::from_bytes(&pallas::Base::random(&mut rng).to_repr())
.unwrap();
let (j, scope) = match recipient_address_type {
AddressType::DefaultExternal => (0u32.into(), zip32::Scope::External),
AddressType::DiversifiedExternal(idx) => (idx, zip32::Scope::External),
AddressType::Internal => (0u32.into(), zip32::Scope::Internal),
};
let (cact, note) = compact_orchard_action(
nullifier_to_reveal,
self.address_at(j, scope),
value,
sender_ovk,
rng,
);
ctx.actions.push(cact);
note.nullifier(self)
}
fn add_logical_action<P: consensus::Parameters, R: TestRngCore>(
&self,
ctx: &mut CompactTx,
_: &P,
_: BlockHeight,
nf_to_reveal_in_spend: Self::Nullifier,
sender_ovk: Option<&Self::OutgoingViewingKey>,
recipient_address_type: AddressType,
value: Zatoshis,
_: u32, rng: &mut R,
) -> Self::Nullifier {
let (j, scope) = match recipient_address_type {
AddressType::DefaultExternal => (0u32.into(), zip32::Scope::External),
AddressType::DiversifiedExternal(idx) => (idx, zip32::Scope::External),
AddressType::Internal => (0u32.into(), zip32::Scope::Internal),
};
let (cact, note) = compact_orchard_action(
nf_to_reveal_in_spend,
self.address_at(j, scope),
value,
sender_ovk,
rng,
);
ctx.actions.push(cact);
note.nullifier(self)
}
}
#[cfg(feature = "orchard")]
#[derive(Clone)]
pub struct IronwoodFvk(pub ::orchard::keys::FullViewingKey);
#[cfg(feature = "orchard")]
impl TestFvk for IronwoodFvk {
type Nullifier = ::orchard::note::Nullifier;
type OutgoingViewingKey = ::orchard::keys::OutgoingViewingKey;
fn to_ovk(&self, scope: zip32::Scope) -> Self::OutgoingViewingKey {
self.0.to_ovk(scope)
}
fn ovk_bytes(&self, scope: zip32::Scope) -> [u8; 32] {
*self.0.to_ovk(scope).as_ref()
}
fn add_spend<R: TestRngCore>(
&self,
ctx: &mut CompactTx,
nullifier_to_reveal: Self::Nullifier,
rng: &mut R,
) {
let recipient = loop {
let mut bytes = [0; 32];
RngCore08::fill_bytes(rng, &mut bytes);
let sk = ::orchard::keys::SpendingKey::from_bytes(bytes);
if sk.is_some().into() {
break ::orchard::keys::FullViewingKey::from(&sk.unwrap())
.address_at(0u32, zip32::Scope::External);
}
};
let (cact, _) =
compact_ironwood_action(nullifier_to_reveal, recipient, Zatoshis::ZERO, None, rng);
ctx.ironwood_actions.push(cact);
}
fn add_output<P: consensus::Parameters, R: TestRngCore>(
&self,
ctx: &mut CompactTx,
_: &P,
_: BlockHeight,
sender_ovk: Option<&Self::OutgoingViewingKey>,
recipient_address_type: AddressType,
value: Zatoshis,
_: u32, mut rng: &mut R,
) -> Self::Nullifier {
let nullifier_to_reveal =
::orchard::note::Nullifier::from_bytes(&pallas::Base::random(&mut rng).to_repr())
.unwrap();
let (j, scope) = match recipient_address_type {
AddressType::DefaultExternal => (0u32.into(), zip32::Scope::External),
AddressType::DiversifiedExternal(idx) => (idx, zip32::Scope::External),
AddressType::Internal => (0u32.into(), zip32::Scope::Internal),
};
let (cact, note) = compact_ironwood_action(
nullifier_to_reveal,
self.0.address_at(j, scope),
value,
sender_ovk,
rng,
);
ctx.ironwood_actions.push(cact);
note.nullifier(&self.0)
}
fn add_logical_action<P: consensus::Parameters, R: TestRngCore>(
&self,
ctx: &mut CompactTx,
_: &P,
_: BlockHeight,
nf_to_reveal_in_spend: Self::Nullifier,
sender_ovk: Option<&Self::OutgoingViewingKey>,
recipient_address_type: AddressType,
value: Zatoshis,
_: u32, rng: &mut R,
) -> Self::Nullifier {
let (j, scope) = match recipient_address_type {
AddressType::DefaultExternal => (0u32.into(), zip32::Scope::External),
AddressType::DiversifiedExternal(idx) => (idx, zip32::Scope::External),
AddressType::Internal => (0u32.into(), zip32::Scope::Internal),
};
let (cact, note) = compact_ironwood_action(
nf_to_reveal_in_spend,
self.0.address_at(j, scope),
value,
sender_ovk,
rng,
);
ctx.ironwood_actions.push(cact);
note.nullifier(&self.0)
}
}
#[derive(Clone, Copy, Debug)]
pub enum AddressType {
DefaultExternal,
#[allow(dead_code)]
DiversifiedExternal(DiversifierIndex),
Internal,
}
fn compact_sapling_output<P: consensus::Parameters, R: TestRngCore>(
params: &P,
height: BlockHeight,
recipient: ::sapling::PaymentAddress,
value: Zatoshis,
sender_ovk: Option<::sapling::keys::OutgoingViewingKey>,
rng: &mut R,
) -> (CompactSaplingOutput, ::sapling::Note) {
let rseed = generate_random_rseed(zip212_enforcement(params, height), rng);
let note = ::sapling::Note::from_parts(
recipient,
::sapling::value::NoteValue::from_raw(value.into_u64()),
rseed,
);
let encryptor = sapling_note_encryption(
sender_ovk,
note.clone(),
MemoBytes::empty().into_bytes(),
rng,
);
let cmu = note.cmu().to_bytes().to_vec();
let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk()).0.to_vec();
let enc_ciphertext = encryptor.encrypt_note_plaintext();
(
CompactSaplingOutput {
cmu,
ephemeral_key,
ciphertext: enc_ciphertext[..52].to_vec(),
},
note,
)
}
#[cfg(feature = "orchard")]
fn compact_orchard_action<R: TestRngCore>(
nf_old: ::orchard::note::Nullifier,
recipient: ::orchard::Address,
value: Zatoshis,
sender_ovk: Option<&::orchard::keys::OutgoingViewingKey>,
rng: &mut R,
) -> (CompactOrchardAction, ::orchard::Note) {
let (compact_action, note) = ::orchard::note_encryption::testing::fake_compact_action(
rng,
nf_old,
recipient,
::orchard::value::NoteValue::from_raw(value.into_u64()),
sender_ovk.cloned(),
);
(
CompactOrchardAction {
nullifier: compact_action.nullifier().to_bytes().to_vec(),
cmx: compact_action.cmx().to_bytes().to_vec(),
ephemeral_key:
ShieldedOutput::<::orchard::note_encryption::OrchardDomain, 52>::ephemeral_key(
&compact_action,
)
.0
.to_vec(),
ciphertext:
ShieldedOutput::<::orchard::note_encryption::OrchardDomain, 52>::enc_ciphertext(
&compact_action,
)[..52]
.to_vec(),
},
note,
)
}
#[cfg(feature = "orchard")]
fn compact_ironwood_action<R: TestRngCore>(
nf_old: ::orchard::note::Nullifier,
recipient: ::orchard::Address,
value: Zatoshis,
sender_ovk: Option<&::orchard::keys::OutgoingViewingKey>,
rng: &mut R,
) -> (CompactOrchardAction, ::orchard::Note) {
let rho = Rho::from_bytes(&nf_old.to_bytes()).unwrap();
let rseed = loop {
let mut bytes = [0u8; 32];
RngCore08::fill_bytes(rng, &mut bytes);
if let Some(rseed) = Option::from(RandomSeed::from_bytes(bytes, &rho)) {
break rseed;
}
};
let note = OrchardNote::from_parts(
recipient,
::orchard::value::NoteValue::from_raw(value.into_u64()),
rho,
rseed,
NoteVersion::V3,
)
.unwrap();
let encryptor = IronwoodNoteEncryption::new(sender_ovk.cloned(), note, [0u8; 512]);
let cmx = ExtractedNoteCommitment::from(note.commitment());
let ephemeral_key = IronwoodDomain::epk_bytes(encryptor.epk());
let enc_ciphertext = encryptor.encrypt_note_plaintext();
(
CompactOrchardAction {
nullifier: nf_old.to_bytes().to_vec(),
cmx: cmx.to_bytes().to_vec(),
ephemeral_key: ephemeral_key.0.to_vec(),
ciphertext: enc_ciphertext[..52].to_vec(),
},
note,
)
}
fn fake_compact_tx<R: TestRngCore>(rng: &mut R) -> CompactTx {
let mut ctx = CompactTx::default();
let mut txid = vec![0; 32];
RngCore08::fill_bytes(rng, &mut txid);
ctx.txid = txid;
ctx
}
#[derive(Clone)]
pub struct FakeCompactOutput<Fvk> {
recipient_fvk: Fvk,
recipient_address_type: AddressType,
value: Zatoshis,
}
impl<Fvk> FakeCompactOutput<Fvk> {
pub fn new(recipient_fvk: Fvk, recipient_address_type: AddressType, value: Zatoshis) -> Self {
Self {
recipient_fvk,
recipient_address_type,
value,
}
}
pub fn random<R: RngCore08>(rng: &mut R, recipient_fvk: Fvk) -> Self {
Self {
recipient_fvk,
recipient_address_type: AddressType::DefaultExternal,
value: Zatoshis::const_from_u64(rng.gen_range(10000..1000000)),
}
}
}
#[allow(clippy::too_many_arguments)]
fn fake_compact_block<P: consensus::Parameters, Fvk: TestFvk>(
params: &P,
height: BlockHeight,
prev_hash: BlockHash,
sender_ovk: Option<&Fvk::OutgoingViewingKey>,
outputs: &[FakeCompactOutput<Fvk>],
initial_sapling_tree_size: u32,
initial_orchard_tree_size: u32,
initial_ironwood_tree_size: u32,
mut rng: impl TestRngCore,
) -> (CompactBlock, Vec<Fvk::Nullifier>) {
let mut ctx = fake_compact_tx(&mut rng);
let mut nfs = vec![];
for output in outputs {
let nf = output.recipient_fvk.add_output(
&mut ctx,
params,
height,
sender_ovk,
output.recipient_address_type,
output.value,
initial_sapling_tree_size,
&mut rng,
);
nfs.push(nf);
}
let cb = fake_compact_block_from_compact_tx(
ctx,
height,
prev_hash,
initial_sapling_tree_size,
initial_orchard_tree_size,
initial_ironwood_tree_size,
rng,
);
(cb, nfs)
}
#[allow(clippy::too_many_arguments)]
fn fake_compact_block_from_tx(
height: BlockHeight,
prev_hash: BlockHash,
tx_index: usize,
tx: &Transaction,
initial_sapling_tree_size: u32,
initial_orchard_tree_size: u32,
initial_ironwood_tree_size: u32,
rng: impl RngCore08,
) -> CompactBlock {
let mut ctx = CompactTx {
index: tx_index as u64,
txid: tx.txid().as_ref().to_vec(),
..Default::default()
};
if let Some(bundle) = tx.sapling_bundle() {
for spend in bundle.shielded_spends() {
ctx.spends.push(spend.into());
}
for output in bundle.shielded_outputs() {
ctx.outputs.push(output.into());
}
}
#[cfg(feature = "orchard")]
if let Some(bundle) = tx.orchard_bundle() {
for action in bundle.actions() {
ctx.actions.push(action.into());
}
}
#[cfg(feature = "orchard")]
if let Some(bundle) = tx.ironwood_bundle() {
for action in bundle.actions() {
ctx.ironwood_actions.push(action.into());
}
}
fake_compact_block_from_compact_tx(
ctx,
height,
prev_hash,
initial_sapling_tree_size,
initial_orchard_tree_size,
initial_ironwood_tree_size,
rng,
)
}
#[allow(clippy::too_many_arguments)]
fn fake_compact_block_spending<P: consensus::Parameters, Fvk: TestFvk>(
params: &P,
height: BlockHeight,
prev_hash: BlockHash,
(nf, in_value): (Fvk::Nullifier, Zatoshis),
sender_fvk: &Fvk,
to: Address,
value: Zatoshis,
initial_sapling_tree_size: u32,
initial_orchard_tree_size: u32,
initial_ironwood_tree_size: u32,
mut rng: impl TestRngCore,
) -> CompactBlock {
let mut ctx = fake_compact_tx(&mut rng);
sender_fvk.add_logical_action(
&mut ctx,
params,
height,
nf,
Some(&sender_fvk.to_ovk(zip32::Scope::Internal)),
AddressType::Internal,
(in_value - value).unwrap(),
initial_sapling_tree_size,
&mut rng,
);
let ovk_bytes = sender_fvk.ovk_bytes(zip32::Scope::External);
match to {
Address::Sapling(recipient) => ctx.outputs.push(
compact_sapling_output(
params,
height,
recipient,
value,
Some(::sapling::keys::OutgoingViewingKey(ovk_bytes)),
&mut rng,
)
.0,
),
Address::Transparent(_) | Address::Tex(_) => {
panic!("transparent addresses not supported in compact blocks")
}
Address::Unified(ua) => {
let mut done = false;
#[cfg(feature = "orchard")]
if let Some(recipient) = ua.orchard() {
let nullifier = ::orchard::note::Nullifier::from_bytes(
&pallas::Base::random(&mut rng).to_repr(),
)
.unwrap();
ctx.actions.push(
compact_orchard_action(
nullifier,
*recipient,
value,
Some(&::orchard::keys::OutgoingViewingKey::from(ovk_bytes)),
&mut rng,
)
.0,
);
done = true;
}
if !done && let Some(recipient) = ua.sapling() {
ctx.outputs.push(
compact_sapling_output(
params,
height,
*recipient,
value,
Some(::sapling::keys::OutgoingViewingKey(ovk_bytes)),
&mut rng,
)
.0,
);
done = true;
}
if !done {
panic!("No supported shielded receiver to send funds to");
}
}
}
fake_compact_block_from_compact_tx(
ctx,
height,
prev_hash,
initial_sapling_tree_size,
initial_orchard_tree_size,
initial_ironwood_tree_size,
rng,
)
}
fn fake_compact_block_from_compact_tx(
ctx: CompactTx,
height: BlockHeight,
prev_hash: BlockHash,
initial_sapling_tree_size: u32,
initial_orchard_tree_size: u32,
initial_ironwood_tree_size: u32,
mut rng: impl RngCore08,
) -> CompactBlock {
let mut cb = CompactBlock {
hash: {
let mut hash = vec![0; 32];
rng.fill_bytes(&mut hash);
hash
},
height: height.into(),
..Default::default()
};
cb.prev_hash.extend_from_slice(&prev_hash.0);
cb.vtx.push(ctx);
cb.chain_metadata = Some(compact_formats::ChainMetadata {
sapling_commitment_tree_size: initial_sapling_tree_size
+ cb.vtx.iter().map(|tx| tx.outputs.len() as u32).sum::<u32>(),
orchard_commitment_tree_size: initial_orchard_tree_size
+ cb.vtx.iter().map(|tx| tx.actions.len() as u32).sum::<u32>(),
ironwood_commitment_tree_size: initial_ironwood_tree_size
+ cb.vtx
.iter()
.map(|tx| tx.ironwood_actions.len() as u32)
.sum::<u32>(),
});
cb
}
pub trait CacheInsertionResult {
fn txids(&self) -> &[TxId];
}
pub trait TestCache {
type BsError: core::fmt::Debug;
type BlockSource: BlockSource<Error = Self::BsError>;
type InsertResult: CacheInsertionResult;
fn block_source(&self) -> &Self::BlockSource;
fn insert(&mut self, cb: &CompactBlock) -> Self::InsertResult;
fn truncate_to_height(&mut self, height: BlockHeight);
}
pub struct NoteCommitments {
sapling: Vec<::sapling::Node>,
#[cfg(feature = "orchard")]
orchard: Vec<MerkleHashOrchard>,
}
impl NoteCommitments {
pub fn from_compact_block(cb: &CompactBlock) -> Self {
NoteCommitments {
sapling: cb
.vtx
.iter()
.flat_map(|tx| {
tx.outputs
.iter()
.map(|out| ::sapling::Node::from_cmu(&out.cmu().unwrap()))
})
.collect(),
#[cfg(feature = "orchard")]
orchard: cb
.vtx
.iter()
.flat_map(|tx| {
tx.actions
.iter()
.map(|act| MerkleHashOrchard::from_cmx(&act.cmx().unwrap()))
})
.collect(),
}
}
#[allow(dead_code)]
pub fn sapling(&self) -> &[::sapling::Node] {
self.sapling.as_ref()
}
#[cfg(feature = "orchard")]
pub fn orchard(&self) -> &[MerkleHashOrchard] {
self.orchard.as_ref()
}
}
pub struct MockWalletDb {
pub network: Network,
pub sapling_tree: ShardTree<
MemoryShardStore<::sapling::Node, BlockHeight>,
{ SAPLING_SHARD_HEIGHT * 2 },
SAPLING_SHARD_HEIGHT,
>,
#[cfg(feature = "orchard")]
pub orchard_tree: ShardTree<
MemoryShardStore<::orchard::tree::MerkleHashOrchard, BlockHeight>,
{ ORCHARD_SHARD_HEIGHT * 2 },
ORCHARD_SHARD_HEIGHT,
>,
account_ids: Vec<u32>,
addresses_by_account: HashMap<u32, Vec<AddressInfo>>,
ufvks_by_account: HashMap<u32, UnifiedFullViewingKey>,
}
impl MockWalletDb {
pub fn new(network: Network) -> Self {
Self {
network,
sapling_tree: ShardTree::new(MemoryShardStore::empty(), 100),
#[cfg(feature = "orchard")]
orchard_tree: ShardTree::new(MemoryShardStore::empty(), 100),
account_ids: vec![],
addresses_by_account: HashMap::new(),
ufvks_by_account: HashMap::new(),
}
}
#[cfg(test)]
pub(crate) fn from_account_addresses(
network: Network,
entries: impl IntoIterator<Item = (u32, Vec<AddressInfo>)>,
) -> Self {
let mut wallet = Self::new(network);
for (account_id, addresses) in entries {
wallet.account_ids.push(account_id);
wallet.addresses_by_account.insert(account_id, addresses);
}
wallet.account_ids.sort_unstable();
wallet
}
#[cfg(test)]
pub(crate) fn from_account_ufvks(
network: Network,
entries: impl IntoIterator<Item = (u32, UnifiedFullViewingKey)>,
) -> Self {
let mut wallet = Self::new(network);
for (account_id, ufvk) in entries {
wallet.account_ids.push(account_id);
wallet.ufvks_by_account.insert(account_id, ufvk);
}
wallet.account_ids.sort_unstable();
wallet
}
}
impl InputSource for MockWalletDb {
type Error = ();
type NoteRef = u32;
type AccountId = u32;
fn anchor_computable(
&self,
protocol: ShieldedPool,
height: BlockHeight,
) -> Result<bool, Self::Error> {
match protocol {
ShieldedPool::Sapling => Ok(self
.sapling_tree
.store()
.get_checkpoint(&height)
.map_err(|_| ())?
.is_some()),
#[cfg(feature = "orchard")]
ShieldedPool::Orchard => Ok(self
.orchard_tree
.store()
.get_checkpoint(&height)
.map_err(|_| ())?
.is_some()),
_ => Ok(false),
}
}
fn get_spendable_note(
&self,
_txid: &TxId,
_protocol: ShieldedPool,
_index: u32,
_target_height: TargetHeight,
_lock_filter: LockFilter<'_>,
) -> Result<Option<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
Ok(None)
}
fn select_spendable_notes(
&self,
_account: Self::AccountId,
_target_value: TargetValue,
_sources: &[ShieldedPool],
_target_height: TargetHeight,
_confirmations_policy: ConfirmationsPolicy,
_exclude: &[Self::NoteRef],
_lock_filter: LockFilter<'_>,
) -> Result<ReceivedNotes<Self::NoteRef>, Self::Error> {
Ok(ReceivedNotes::empty())
}
fn select_unspent_notes(
&self,
_account: Self::AccountId,
_sources: &[ShieldedPool],
_target_height: TargetHeight,
_exclude: &[Self::NoteRef],
_lock_filter: LockFilter<'_>,
) -> Result<ReceivedNotes<Self::NoteRef>, Self::Error> {
Err(())
}
fn get_account_metadata(
&self,
_account: Self::AccountId,
_selector: &NoteFilter,
_target_height: TargetHeight,
_exclude: &[Self::NoteRef],
_lock_filter: LockFilter<'_>,
) -> Result<AccountMeta, Self::Error> {
Err(())
}
}
impl WalletRead for MockWalletDb {
type Error = ();
type AccountId = u32;
type Account = (Self::AccountId, UnifiedFullViewingKey, BlockHeight);
fn get_account_ids(&self) -> Result<Vec<Self::AccountId>, Self::Error> {
Ok(self.account_ids.clone())
}
fn get_account(
&self,
account_id: Self::AccountId,
) -> Result<Option<Self::Account>, Self::Error> {
Ok(self
.ufvks_by_account
.get(&account_id)
.cloned()
.map(|ufvk| (account_id, ufvk, BlockHeight::from(1))))
}
fn get_derived_account(
&self,
_derivation: &Zip32Derivation,
) -> Result<Option<Self::Account>, Self::Error> {
Ok(None)
}
fn validate_seed(
&self,
_account_id: Self::AccountId,
_seed: &SecretVec<u8>,
) -> Result<bool, Self::Error> {
Ok(false)
}
fn seed_relevance_to_derived_accounts(
&self,
_seed: &SecretVec<u8>,
) -> Result<SeedRelevance<Self::AccountId>, Self::Error> {
Ok(SeedRelevance::NoAccounts)
}
fn get_account_for_ufvk(
&self,
_ufvk: &UnifiedFullViewingKey,
) -> Result<Option<Self::Account>, Self::Error> {
Ok(None)
}
fn list_addresses(&self, account: Self::AccountId) -> Result<Vec<AddressInfo>, Self::Error> {
Ok(self
.addresses_by_account
.get(&account)
.cloned()
.unwrap_or_default())
}
fn find_account_for_address<P: consensus::Parameters>(
&self,
params: &P,
address: &zcash_keys::address::Address,
) -> Result<Option<Self::AccountId>, super::error::FindAccountForAddressError<Self::Error>>
{
super::defaults::find_account_for_address(self, params, address)
}
fn get_last_generated_address_matching(
&self,
_account: Self::AccountId,
_request: UnifiedAddressRequest,
) -> Result<Option<UnifiedAddress>, Self::Error> {
Ok(None)
}
fn get_account_birthday(&self, _account: Self::AccountId) -> Result<BlockHeight, Self::Error> {
Err(())
}
fn get_wallet_birthday(&self) -> Result<Option<BlockHeight>, Self::Error> {
Ok(None)
}
fn get_wallet_recover_until(&self) -> Result<Option<BlockHeight>, Self::Error> {
Ok(None)
}
fn get_wallet_summary(
&self,
_confirmations_policy: ConfirmationsPolicy,
) -> Result<Option<WalletSummary<Self::AccountId>>, Self::Error> {
Ok(None)
}
fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
Ok(None)
}
fn get_block_hash(&self, _block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error> {
Ok(None)
}
fn block_metadata(&self, _height: BlockHeight) -> Result<Option<BlockMetadata>, Self::Error> {
Ok(None)
}
fn block_fully_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
Ok(None)
}
fn get_max_height_hash(&self) -> Result<Option<(BlockHeight, BlockHash)>, Self::Error> {
Ok(None)
}
fn block_max_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
Ok(None)
}
fn suggest_scan_ranges(&self) -> Result<Vec<ScanRange>, Self::Error> {
Ok(vec![])
}
fn get_target_and_anchor_heights(
&self,
_min_confirmations: NonZeroU32,
) -> Result<Option<(TargetHeight, BlockHeight)>, Self::Error> {
Ok(None)
}
fn get_tx_height(&self, _txid: TxId) -> Result<Option<BlockHeight>, Self::Error> {
Ok(None)
}
fn get_unified_full_viewing_keys(
&self,
) -> Result<HashMap<Self::AccountId, UnifiedFullViewingKey>, Self::Error> {
Ok(HashMap::new())
}
fn get_memo(&self, _id_note: NoteId) -> Result<Option<Memo>, Self::Error> {
Ok(None)
}
fn get_transaction(&self, _txid: TxId) -> Result<Option<Transaction>, Self::Error> {
Ok(None)
}
fn get_sapling_nullifiers(
&self,
_query: NullifierQuery,
) -> Result<Vec<(Self::AccountId, ::sapling::Nullifier)>, Self::Error> {
Ok(Vec::new())
}
#[cfg(feature = "orchard")]
fn get_orchard_nullifiers(
&self,
_query: NullifierQuery,
) -> Result<Vec<(Self::AccountId, ::orchard::note::Nullifier)>, Self::Error> {
Ok(Vec::new())
}
#[cfg(feature = "orchard")]
fn get_ironwood_nullifiers(
&self,
_query: NullifierQuery,
) -> Result<Vec<(Self::AccountId, ::orchard::note::Nullifier)>, Self::Error> {
Ok(Vec::new())
}
#[cfg(feature = "transparent-inputs")]
fn get_transparent_receivers(
&self,
_account: Self::AccountId,
_include_change: bool,
_include_standalone: bool,
) -> Result<HashMap<TransparentAddress, TransparentAddressMetadata>, Self::Error> {
Ok(HashMap::new())
}
#[cfg(feature = "transparent-inputs")]
fn get_transparent_balances(
&self,
_account: Self::AccountId,
_target_height: TargetHeight,
_confirmations_policy: ConfirmationsPolicy,
) -> Result<TransparentBalances, Self::Error> {
Ok(HashMap::new())
}
#[cfg(feature = "transparent-inputs")]
fn get_transparent_address_metadata(
&self,
_account: Self::AccountId,
_address: &TransparentAddress,
) -> Result<Option<TransparentAddressMetadata>, Self::Error> {
Ok(None)
}
#[cfg(feature = "transparent-inputs")]
fn utxo_query_height(&self, _account: Self::AccountId) -> Result<BlockHeight, Self::Error> {
Ok(BlockHeight::from(0u32))
}
fn transaction_data_requests(&self) -> Result<Vec<TransactionDataRequest>, Self::Error> {
Ok(vec![])
}
fn get_received_outputs(
&self,
_txid: TxId,
_target_height: TargetHeight,
_confirmations_policy: ConfirmationsPolicy,
) -> Result<Vec<ReceivedTransactionOutput>, Self::Error> {
Ok(vec![])
}
}
impl OutputLockStore for MockWalletDb {
type Error = ();
type AccountId = u32;
fn lock_outputs(
&mut self,
_outputs: &[OutputRef],
_owner: LockOwner,
_lock_expiry_height: BlockHeight,
) -> Result<usize, LockError<Self::Error>> {
Ok(0)
}
fn unlock_output(
&mut self,
_output: &OutputRef,
_owner: LockOwner,
) -> Result<bool, Self::Error> {
Ok(false)
}
fn clear_locked_outputs(&mut self, _account: Self::AccountId) -> Result<usize, Self::Error> {
Ok(0)
}
fn get_locked_outputs(&self, _account: Self::AccountId) -> Result<Vec<OutputRef>, Self::Error> {
Ok(Vec::new())
}
}
impl WalletWrite for MockWalletDb {
type UtxoRef = u32;
fn create_account(
&mut self,
_account_name: &str,
seed: &SecretVec<u8>,
_birthday: &AccountBirthday,
_key_source: Option<&str>,
) -> Result<(<Self as WalletRead>::AccountId, UnifiedSpendingKey), <Self as WalletRead>::Error>
{
let account = zip32::AccountId::ZERO;
UnifiedSpendingKey::from_seed(&self.network, seed.expose_secret(), account)
.map(|k| (u32::from(account), k))
.map_err(|_| ())
}
fn import_account_hd(
&mut self,
_account_name: &str,
_seed: &SecretVec<u8>,
_account_index: zip32::AccountId,
_birthday: &AccountBirthday,
_key_source: Option<&str>,
) -> Result<(Self::Account, UnifiedSpendingKey), <Self as WalletRead>::Error> {
todo!()
}
fn import_account_ufvk(
&mut self,
_account_name: &str,
_unified_key: &UnifiedFullViewingKey,
_birthday: &AccountBirthday,
_purpose: AccountPurpose,
_key_source: Option<&str>,
) -> Result<Self::Account, <Self as WalletRead>::Error> {
todo!()
}
fn delete_account(
&mut self,
_account: <Self as WalletRead>::AccountId,
) -> Result<(), <Self as WalletRead>::Error> {
todo!()
}
#[cfg(feature = "transparent-key-import")]
fn import_standalone_transparent_pubkey(
&mut self,
_account: <Self as WalletRead>::AccountId,
_address: secp256k1::PublicKey,
) -> Result<(), <Self as WalletRead>::Error> {
todo!()
}
#[cfg(feature = "transparent-key-import")]
fn import_standalone_transparent_script(
&mut self,
_account: <Self as WalletRead>::AccountId,
_script: script::Redeem,
) -> Result<(), <Self as WalletRead>::Error> {
todo!()
}
fn get_next_available_address(
&mut self,
_account: <Self as WalletRead>::AccountId,
_request: UnifiedAddressRequest,
) -> Result<Option<(UnifiedAddress, DiversifierIndex)>, <Self as WalletRead>::Error> {
Ok(None)
}
fn get_address_for_index(
&mut self,
_account: <Self as WalletRead>::AccountId,
_diversifier_index: DiversifierIndex,
_request: UnifiedAddressRequest,
) -> Result<Option<UnifiedAddress>, <Self as WalletRead>::Error> {
Ok(None)
}
#[allow(clippy::type_complexity)]
fn put_blocks(
&mut self,
_from_state: &ChainState,
_blocks: Vec<ScannedBlock<<Self as WalletRead>::AccountId>>,
) -> Result<(), <Self as WalletRead>::Error> {
Ok(())
}
fn update_chain_tip(
&mut self,
_tip_height: BlockHeight,
) -> Result<(), <Self as WalletRead>::Error> {
Ok(())
}
fn prune_scan_queue_below(
&mut self,
_height: BlockHeight,
_retain_with_priority: Option<ScanPriority>,
) -> Result<u64, <Self as WalletRead>::Error> {
Ok(0)
}
fn store_decrypted_tx(
&mut self,
_received_tx: DecryptedTransaction<Transaction, <Self as WalletRead>::AccountId>,
) -> Result<(), <Self as WalletRead>::Error> {
Ok(())
}
fn set_tx_trust(
&mut self,
_txid: TxId,
_trusted: bool,
) -> Result<(), <Self as WalletRead>::Error> {
Ok(())
}
fn store_transactions_to_be_sent(
&mut self,
_transactions: &[SentTransaction<<Self as WalletRead>::AccountId>],
) -> Result<(), <Self as WalletRead>::Error> {
Ok(())
}
fn truncate_to_height(
&mut self,
_block_height: BlockHeight,
) -> Result<BlockHeight, <Self as WalletRead>::Error> {
Err(())
}
fn truncate_to_chain_state(
&mut self,
_chain_state: ChainState,
) -> Result<(), <Self as WalletRead>::Error> {
Err(())
}
fn rewind_to_chain_state(
&mut self,
_chain_state: ChainState,
_reset_account_birthdays: HashSet<<Self as WalletRead>::AccountId>,
) -> Result<(), RewindError<<Self as WalletRead>::AccountId, <Self as WalletRead>::Error>> {
Err(RewindError::DataSource(()))
}
fn put_received_transparent_utxo(
&mut self,
_output: &WalletTransparentOutput<<Self as WalletRead>::AccountId>,
) -> Result<Self::UtxoRef, <Self as WalletRead>::Error> {
Ok(0)
}
#[cfg(feature = "transparent-inputs")]
fn reserve_next_n_ephemeral_addresses(
&mut self,
_account_id: <Self as WalletRead>::AccountId,
_n: usize,
) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
{
Err(())
}
#[cfg(feature = "transparent-inputs")]
fn reserve_next_n_internal_addresses(
&mut self,
_account_id: <Self as WalletRead>::AccountId,
_n: usize,
) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, <Self as WalletRead>::Error>
{
Err(())
}
fn set_transaction_status(
&mut self,
_txid: TxId,
_status: TransactionStatus,
) -> Result<(), <Self as WalletRead>::Error> {
Ok(())
}
#[cfg(feature = "transparent-inputs")]
fn notify_address_checked(
&mut self,
_request: TransactionsInvolvingAddress,
_as_of_height: BlockHeight,
) -> Result<(), <Self as WalletRead>::Error> {
Ok(())
}
}
impl WalletCommitmentTrees for MockWalletDb {
type Error = Infallible;
type SaplingShardStore<'a> = MemoryShardStore<::sapling::Node, BlockHeight>;
fn with_sapling_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
where
for<'a> F: FnMut(
&'a mut ShardTree<
Self::SaplingShardStore<'a>,
{ ::sapling::NOTE_COMMITMENT_TREE_DEPTH },
SAPLING_SHARD_HEIGHT,
>,
) -> Result<A, E>,
E: From<ShardTreeError<Infallible>>,
{
callback(&mut self.sapling_tree)
}
fn put_sapling_subtree_roots(
&mut self,
start_index: u64,
roots: &[CommitmentTreeRoot<::sapling::Node>],
) -> Result<(), ShardTreeError<Self::Error>> {
self.with_sapling_tree_mut(|t| {
for (root, i) in roots.iter().zip(0u64..) {
let root_addr = incrementalmerkletree::Address::from_parts(
SAPLING_SHARD_HEIGHT.into(),
start_index + i,
);
t.insert(root_addr, *root.root_hash())?;
}
Ok::<_, ShardTreeError<Self::Error>>(())
})?;
Ok(())
}
fn get_sapling_subtree_root(
&mut self,
index: u64,
) -> Result<Option<::sapling::Node>, ShardTreeError<Self::Error>> {
self.with_sapling_tree_mut(|t| {
let addr =
incrementalmerkletree::Address::from_parts(SAPLING_SHARD_HEIGHT.into(), index);
Ok::<_, ShardTreeError<Self::Error>>(
t.store()
.get_shard(addr)
.map_err(ShardTreeError::Storage)?
.and_then(|shard| match shard.root() {
tree if tree.is_leaf() => tree.leaf_value().copied(),
tree => tree.annotation().and_then(|ann| ann.as_deref().copied()),
}),
)
})
}
#[cfg(feature = "orchard")]
type OrchardShardStore<'a> = MemoryShardStore<::orchard::tree::MerkleHashOrchard, BlockHeight>;
#[cfg(feature = "orchard")]
fn with_orchard_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
where
for<'a> F: FnMut(
&'a mut ShardTree<
Self::OrchardShardStore<'a>,
{ ORCHARD_SHARD_HEIGHT * 2 },
ORCHARD_SHARD_HEIGHT,
>,
) -> Result<A, E>,
E: From<ShardTreeError<Self::Error>>,
{
callback(&mut self.orchard_tree)
}
#[cfg(feature = "orchard")]
fn put_orchard_subtree_roots(
&mut self,
start_index: u64,
roots: &[CommitmentTreeRoot<::orchard::tree::MerkleHashOrchard>],
) -> Result<(), ShardTreeError<Self::Error>> {
self.with_orchard_tree_mut(|t| {
for (root, i) in roots.iter().zip(0u64..) {
let root_addr = incrementalmerkletree::Address::from_parts(
ORCHARD_SHARD_HEIGHT.into(),
start_index + i,
);
t.insert(root_addr, *root.root_hash())?;
}
Ok::<_, ShardTreeError<Self::Error>>(())
})?;
Ok(())
}
#[cfg(feature = "orchard")]
fn get_orchard_subtree_root(
&mut self,
index: u64,
) -> Result<Option<::orchard::tree::MerkleHashOrchard>, ShardTreeError<Self::Error>> {
self.with_orchard_tree_mut(|t| {
let addr =
incrementalmerkletree::Address::from_parts(ORCHARD_SHARD_HEIGHT.into(), index);
Ok::<_, ShardTreeError<Self::Error>>(
t.store()
.get_shard(addr)
.map_err(ShardTreeError::Storage)?
.and_then(|shard| match shard.root() {
tree if tree.is_leaf() => tree.leaf_value().copied(),
tree => tree.annotation().and_then(|ann| ann.as_deref().copied()),
}),
)
})
}
}