use std::time::{Duration, SystemTime};
use zcash_client_backend::data_api::testing::{FakeCompactOutput, TestRng};
use super::*;
use crate::util::Clock;
const MAX_DELAY: Duration = Duration::from_secs(75);
fn delayed_state(seed: u64) -> State {
let mut st = state_with_factory(TestDbFactory::default());
let db = st.wallet_mut().db_mut();
db.set_enhance_pir_max_delay(MAX_NOT_BEFORE_DELAY);
db.set_enhancement_mode(EnhancementMode::PrivateIronwood);
db.rng = TestRng::seed_from_u64(seed);
st
}
fn scan_notes(st: &mut State, outputs: usize) -> BlockHeight {
let fvk = IronwoodFvk(OrchardPoolTester::test_account_fvk(st));
let outputs = (0..outputs)
.map(|i| {
FakeCompactOutput::new(
&fvk,
AddressType::DefaultExternal,
Zatoshis::const_from_u64(10_000 + i as u64),
)
})
.collect::<Vec<_>>();
let (height, _, _) = st.generate_next_block_multi(&outputs);
st.scan_cached_blocks(height, 1);
height
}
fn now(st: &State) -> SystemTime {
st.wallet().db().clock.now()
}
fn private_work(st: &State) -> Vec<EnhancePirWork> {
st.wallet()
.db()
.transaction_enhancement_work()
.unwrap()
.into_iter()
.filter_map(|work| match work {
TransactionEnhancementWork::Private(work) => Some(work),
TransactionEnhancementWork::Public(_) => None,
})
.collect()
}
fn waiting(st: &State) -> Vec<(EnhancePirRequest, SystemTime)> {
private_work(st)
.into_iter()
.filter_map(|work| match work {
EnhancePirWork::Waiting {
request,
not_before,
} => Some((request, not_before)),
_ => None,
})
.collect()
}
fn queries(st: &State) -> Vec<EnhancePirRequest> {
private_work(st)
.into_iter()
.filter_map(|work| match work {
EnhancePirWork::Query(request) => Some(request),
_ => None,
})
.collect()
}
fn single_wait(st: &State) -> (EnhancePirRequest, SystemTime) {
let waiting = waiting(st);
assert_eq!(waiting.len(), 1, "one newly queued position waits");
waiting[0]
}
#[test]
fn a_newly_queued_position_is_not_queried_before_its_time() {
let mut st = delayed_state(0);
let queued_at = now(&st);
scan_notes(&mut st, 1);
let (request, not_before) = single_wait(&st);
assert!(queries(&st).is_empty(), "nothing is due at the queue time");
assert!(not_before > queued_at && not_before <= queued_at + MAX_DELAY);
st.wallet().db().clock.tick(
not_before
.duration_since(queued_at)
.unwrap()
.saturating_sub(Duration::from_secs(1)),
);
assert_eq!(single_wait(&st), (request, not_before), "a second early");
st.wallet().db().clock.tick(Duration::from_secs(1));
assert!(waiting(&st).is_empty());
assert_eq!(queries(&st), vec![request], "due at its time");
}
#[test]
fn the_delay_comes_from_the_wallet_random_source() {
let draw = |seed| {
let mut st = delayed_state(seed);
let queued_at = now(&st);
scan_notes(&mut st, 1);
single_wait(&st).1.duration_since(queued_at).unwrap()
};
assert_eq!(draw(1), draw(1), "the same seed gives the same delay");
assert_ne!(draw(1), draw(2), "another seed gives another delay");
let mut st = delayed_state(3);
scan_notes(&mut st, 2);
let waiting = waiting(&st);
assert_eq!(waiting.len(), 2);
assert_ne!(waiting[0].0.position(), waiting[1].0.position());
assert_ne!(waiting[0].1, waiting[1].1);
}
#[test]
fn a_rescan_keeps_the_time_and_a_rewind_draws_a_new_one() {
let mut st = delayed_state(4);
let height = scan_notes(&mut st, 1);
let (request, not_before) = single_wait(&st);
st.wallet().db().clock.tick(Duration::from_secs(30));
st.scan_cached_blocks(height, 1);
assert_eq!(single_wait(&st), (request, not_before), "a rescan keeps it");
st.wallet_mut()
.db_mut()
.truncate_to_height(height - 1)
.unwrap();
assert!(private_work(&st).is_empty());
st.wallet().db().clock.tick(MAX_DELAY * 2);
let requeued_at = now(&st);
st.scan_cached_blocks(height, 1);
let (again, later) = single_wait(&st);
assert_eq!(again, request);
assert!(later > requeued_at, "the rewind deleted the old time");
}
#[test]
fn completing_a_position_deletes_its_time() {
let mut st = delayed_state(5);
scan_notes(&mut st, 1);
let (request, not_before) = single_wait(&st);
st.wallet().db().clock.tick(
not_before
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.saturating_sub(now(&st).duration_since(SystemTime::UNIX_EPOCH).unwrap()),
);
assert_eq!(queries(&st), vec![request]);
finish_incoming(&mut st, request);
assert!(private_work(&st).is_empty());
assert_eq!(st.wallet().db().enhance_pir_session.waits(), 0);
}
#[test]
fn a_fresh_session_draws_the_wait_again_and_keeps_it_within_the_session() {
let mut st = delayed_state(6);
let height = scan_notes(&mut st, 1);
let (request, _) = single_wait(&st);
st.wallet().db().clock.tick(MAX_DELAY * 2);
assert_eq!(queries(&st), vec![request], "the first wait has passed");
st.wallet_mut().db_mut().enhance_pir_session = Default::default();
let restarted_at = now(&st);
let tip = st.wallet().chain_height().unwrap().unwrap();
st.wallet_mut().update_chain_tip(tip).unwrap();
let (again, not_before) = single_wait(&st);
assert_eq!(again, request);
assert!(not_before > restarted_at && not_before <= restarted_at + MAX_DELAY);
assert!(queries(&st).is_empty(), "not due before the new wait");
st.wallet().db().clock.tick(Duration::from_secs(1));
st.scan_cached_blocks(height, 1);
assert_eq!(
single_wait(&st),
(request, not_before),
"the session keeps it across a rescan"
);
}
#[test]
fn a_fresh_session_read_snapshot_keeps_its_deadline_until_the_query_is_due() {
let mut st = delayed_state(7);
scan_notes(&mut st, 1);
st.wallet_mut().db_mut().enhance_pir_session = Default::default();
let first_read = now(&st);
let read = |st: &mut State| {
st.wallet_mut()
.db_mut()
.with_immediate_read_transaction(|db| db.transaction_enhancement_work())
.unwrap()
};
let work = read(&mut st);
let TransactionEnhancementWork::Private(EnhancePirWork::Waiting {
request,
not_before,
}) = work[0]
else {
panic!("a fresh session starts with waiting work");
};
assert_eq!(not_before, first_read + MAX_DELAY);
st.wallet()
.db()
.clock
.tick(MAX_DELAY - Duration::from_secs(1));
assert_eq!(
read(&mut st),
work,
"a later snapshot keeps the first deadline"
);
st.wallet().db().clock.tick(Duration::from_secs(1));
assert_eq!(
read(&mut st),
vec![TransactionEnhancementWork::Private(EnhancePirWork::Query(
request
))]
);
}
#[test]
fn a_failed_read_snapshot_keeps_committed_work_waiting_from_its_first_read() {
let mut st = delayed_state(8);
scan_notes(&mut st, 1);
st.wallet_mut().db_mut().enhance_pir_session = Default::default();
let result = st
.wallet_mut()
.db_mut()
.with_immediate_read_transaction(|db| {
assert!(matches!(
db.transaction_enhancement_work()?.as_slice(),
[TransactionEnhancementWork::Private(
EnhancePirWork::Waiting { .. }
)]
));
Err::<(), _>(SqliteClientError::AccountUnknown)
});
assert!(result.is_err());
st.wallet().db().clock.tick(MAX_DELAY);
let work = st
.wallet_mut()
.db_mut()
.with_immediate_read_transaction(|db| db.transaction_enhancement_work())
.unwrap();
assert!(matches!(
work.as_slice(),
[TransactionEnhancementWork::Private(EnhancePirWork::Query(
_
))]
));
}
#[test]
fn a_rolled_back_write_does_not_publish_fresh_session_waits() {
let mut st = delayed_state(9);
scan_notes(&mut st, 1);
st.wallet_mut().db_mut().enhance_pir_session = Default::default();
let tip = st.wallet().chain_height().unwrap().unwrap();
let result = st.wallet_mut().db_mut().transactionally(|db| {
db.update_chain_tip(tip)?;
assert_eq!(db.enhance_pir_session.waits(), 1);
Err::<(), _>(SqliteClientError::AccountUnknown)
});
assert!(result.is_err());
assert_eq!(
st.wallet().db().enhance_pir_session.waits(),
0,
"rolled-back deadlines stay private to their transaction"
);
let first_read = now(&st);
let work = st
.wallet_mut()
.db_mut()
.with_immediate_read_transaction(|db| db.transaction_enhancement_work())
.unwrap();
assert!(matches!(work.as_slice(),
[TransactionEnhancementWork::Private(EnhancePirWork::Waiting { not_before, .. })]
if *not_before == first_read + MAX_DELAY));
}