Merge pull request #1736 from nuttycom/feature/taddr_fetch_scheduling

This commit is contained in:
Kris Nuttycombe 2025-03-15 17:39:38 -06:00 committed by GitHub
commit 4130409eb0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 797 additions and 189 deletions

12
Cargo.lock generated
View File

@ -3391,6 +3391,16 @@ dependencies = [
"getrandom",
]
[[package]]
name = "rand_distr"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
dependencies = [
"num-traits",
"rand",
]
[[package]]
name = "rand_xorshift"
version = "0.3.0"
@ -6241,8 +6251,10 @@ dependencies = [
"pasta_curves",
"proptest",
"prost",
"rand",
"rand_chacha",
"rand_core",
"rand_distr",
"regex",
"rusqlite",
"sapling-crypto",

View File

@ -82,6 +82,7 @@ nonempty = { version = "0.11", default-features = false }
# CSPRNG
rand = { version = "0.8", default-features = false }
rand_core = { version = "0.6", default-features = false }
rand_distr = { version = "0.4", default-features = false }
# Currency conversions
rust_decimal = { version = "1.35", default-features = false, features = ["serde"] }

View File

@ -841,6 +841,12 @@ user-id = 169181 # Kris Nuttycombe (nuttycom)
start = "2024-08-12"
end = "2025-08-12"
[[trusted.pczt]]
criteria = "safe-to-deploy"
user-id = 6289 # Jack Grigg (str4d)
start = "2024-10-08"
end = "2026-03-13"
[[trusted.pczt]]
criteria = "safe-to-deploy"
user-id = 169181 # Kris Nuttycombe (nuttycom)

View File

@ -257,18 +257,6 @@ criteria = "safe-to-deploy"
version = "0.4.38"
criteria = "safe-to-deploy"
[[exemptions.ciborium]]
version = "0.2.1"
criteria = "safe-to-run"
[[exemptions.ciborium-io]]
version = "0.2.1"
criteria = "safe-to-run"
[[exemptions.ciborium-ll]]
version = "0.2.1"
criteria = "safe-to-run"
[[exemptions.coarsetime]]
version = "0.1.34"
criteria = "safe-to-deploy"

View File

@ -1,14 +1,6 @@
# cargo-vet imports lock
[[unpublished.equihash]]
version = "0.2.2"
audited_as = "0.2.1"
[[unpublished.pczt]]
version = "0.2.1"
audited_as = "0.2.0"
[[publisher.bumpalo]]
version = "3.16.0"
when = "2024-04-08"
@ -24,11 +16,11 @@ user-login = "jrmuizel"
user-name = "Jeff Muizelaar"
[[publisher.equihash]]
version = "0.2.1"
when = "2025-02-21"
user-id = 169181
user-login = "nuttycom"
user-name = "Kris Nuttycombe"
version = "0.2.2"
when = "2025-03-05"
user-id = 6289
user-login = "str4d"
user-name = "Jack Grigg"
[[publisher.f4jumble]]
version = "0.1.1"
@ -93,11 +85,11 @@ user-login = "nuttycom"
user-name = "Kris Nuttycombe"
[[publisher.pczt]]
version = "0.2.0"
when = "2025-02-21"
user-id = 169181
user-login = "nuttycom"
user-name = "Kris Nuttycombe"
version = "0.2.1"
when = "2025-03-05"
user-id = 6289
user-login = "str4d"
user-name = "Jack Grigg"
[[publisher.sapling-crypto]]
version = "0.5.0"
@ -966,6 +958,24 @@ criteria = "safe-to-deploy"
version = "1.0.0"
aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT"
[[audits.google.audits.ciborium]]
who = "Daniel Verkamp <dverkamp@chromium.org>"
criteria = "safe-to-run"
version = "0.2.2"
aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT"
[[audits.google.audits.ciborium-io]]
who = "Daniel Verkamp <dverkamp@chromium.org>"
criteria = "safe-to-run"
version = "0.2.2"
aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT"
[[audits.google.audits.ciborium-ll]]
who = "Daniel Verkamp <dverkamp@chromium.org>"
criteria = "safe-to-run"
version = "0.2.2"
aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT"
[[audits.google.audits.clap]]
who = "Lukasz Anforowicz <lukasza@chromium.org>"
criteria = "safe-to-deploy"
@ -2365,6 +2375,16 @@ criteria = "safe-to-deploy"
delta = "0.6.3 -> 0.6.4"
aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml"
[[audits.mozilla.audits.rand_distr]]
who = "Ben Dean-Kawamura <bdk@mozilla.com>"
criteria = "safe-to-deploy"
version = "0.4.3"
notes = """
Simple crate that extends `rand`. It has little unsafe code and uses Miri to test it.
As far as I can tell, it does not have any file IO or network access.
"""
aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml"
[[audits.mozilla.audits.rayon]]
who = "Josh Stone <jistone@redhat.com>"
criteria = "safe-to-deploy"

View File

@ -13,6 +13,7 @@ and this library adheres to Rust's notion of
- `zcash_client_backend::data_api::testing`:
- `struct transparent::GapLimits`
- `transparent::gap_limits` high-level test for gap limit handling
- `zcash_client_backend::data_api::{TransactionStatusFilter, OutputStatusFilter}`
### Changed
- `zcash_client_backend::data_api::WalletRead`:
@ -38,6 +39,9 @@ and this library adheres to Rust's notion of
`UnifiedAddressRequest` argument is now non-optional; use
`UnifiedAddressRequest::AllAvailableKeys` to indicate that all available
keys should be used to generate receivers instead of `None`.
- `TransactionDataRequest::SpendsFromAddress` has been renamed to
`TransactionDataRequest::TransactionsInvolvingAddress` and has added struct
fields `request_at`, `tx_status_filter`, and `output_status_filter`.
### Removed
- `zcash_client_backend::data_api::GAP_LIMIT` gap limits are now configured

View File

@ -97,6 +97,7 @@ use crate::{
use {
crate::wallet::TransparentAddressMetadata,
std::ops::Range,
std::time::SystemTime,
transparent::{
address::TransparentAddress,
bundle::OutPoint,
@ -747,6 +748,31 @@ pub struct SpendableNotes<NoteRef> {
orchard: Vec<ReceivedNote<NoteRef, orchard::note::Note>>,
}
/// A type describing the mined-ness of transactions that should be returned in response to a
/// [`TransactionDataRequest`].
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg(feature = "transparent-inputs")]
pub enum TransactionStatusFilter {
/// Only mined transactions should be returned.
Mined,
/// Only mempool transactions should be returned.
Mempool,
/// Both mined transactions and transactions in the mempool should be returned.
All,
}
/// A type used to filter transactions to be returned in response to a [`TransactionDataRequest`],
/// in terms of the spentness of the transaction's transparent outputs.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg(feature = "transparent-inputs")]
pub enum OutputStatusFilter {
/// Only transactions that have currently-unspent transparent outputs should be returned.
Unspent,
/// All transactions corresponding to the data request should be returned, irrespective of
/// whether or not those transactions produce transparent outputs that are currently unspent.
All,
}
/// A request for transaction data enhancement, spentness check, or discovery
/// of spends from a given transparent address within a specific block range.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@ -791,10 +817,30 @@ pub enum TransactionDataRequest {
///
/// [`GetTaddressTxids`]: crate::proto::service::compact_tx_streamer_client::CompactTxStreamerClient::get_taddress_txids
#[cfg(feature = "transparent-inputs")]
SpendsFromAddress {
TransactionsInvolvingAddress {
/// The address to request transactions and/or UTXOs for.
address: TransparentAddress,
/// Only transactions mined at heights greater than or equal to this height should be
/// returned.
block_range_start: BlockHeight,
/// Only transactions mined at heights less than this height should be returned.
block_range_end: Option<BlockHeight>,
/// If a `request_at` time is set, the caller evaluating this request should attempt to
/// retrieve transaction data related to the specified address at a time that is as close
/// as practical to the specified instant, and in a fashion that decorrelates this request
/// to a light wallet server from other requests made by the same caller.
///
/// This may be ignored by callers that are able to satisfy the request without exposing
/// correlations between addresses to untrusted parties; for example, a wallet application
/// that uses a private, trusted-for-privacy supplier of chain data can safely ignore this
/// field.
request_at: Option<SystemTime>,
/// The caller should respond to this request only with transactions that conform to the
/// specified transaction status filter.
tx_status_filter: TransactionStatusFilter,
/// The caller should respond to this request only with transactions containing outputs
/// that conform to the specified output status filter.
output_status_filter: OutputStatusFilter,
},
}

View File

@ -11,15 +11,23 @@ and this library adheres to Rust's notion of
- `zcash_client_sqlite::WalletDb::with_gap_limits`
- `zcash_client_sqlite::GapLimits`
- `zcash_client_sqlite::util`
- `zcash_client_sqlite::schedule_ephemeral_address_checks` has been added under
the `transparent-inputs` feature flag.
- `zcash_client_sqlite::wallet::transparent::SchedulingError`
### Changed
- `zcash_client_sqlite::WalletDb` has an added `Clock` field and corresponding
type parameter. Tests that make use of a `WalletDb` instance now use a
`zcash_client_sqlite::util::FixedClock` for their clock instance, and the
following methods have been changed to accept an additional parameter as a
result:
- `WalletDb::for_path`
- `WalletDb::from_connection`
- `zcash_client_sqlite::WalletDb` has added fields and type parameters:
- a `clock` field and corresponding type parameter. Tests that make use of
`WalletDb` now use a `zcash_client_sqlite::util::FixedClock` for this
field value.
- an `rng` field and corresponding type parameter. Tests that make use of
`WalletDb` now use a `ChaChaRng` value initialized with the all-zeros
seed for this field value.
- the following methods have been changed to accept additional parameters
as a result of these changes:
- `WalletDb::for_path`
- `WalletDb::from_connection`
- `wallet::init::init_wallet_db` has additional type constraints
- `zcash_client_sqlite::WalletDb::get_address_for_index` now returns some of
its failure modes via `Err(SqliteClientError::AddressGeneration)` instead of
`Ok(None)`.
@ -31,6 +39,7 @@ and this library adheres to Rust's notion of
to the transparent address index, it also contains the key scope
involved when the error was encountered.
- A new `DiversifierIndexReuse` variant has been added.
- A new `Scheduling` variant has been added.
- Each row returned from the `v_received_outputs` view now exposes an
internal identifier for the address that received that output. This should
be ignored by external consumers of this view.

View File

@ -80,6 +80,9 @@ regex = "1.4"
# (Breaking upgrades to these are usually backwards-compatible, but check MSRVs.)
document-features.workspace = true
maybe-rayon.workspace = true
rand_core.workspace = true
rand_distr.workspace = true
rand.workspace = true
[dev-dependencies]
ambassador.workspace = true

View File

@ -17,6 +17,7 @@ use crate::{wallet::commitment_tree, AccountUuid};
#[cfg(feature = "transparent-inputs")]
use {
crate::wallet::transparent::SchedulingError,
::transparent::{address::TransparentAddress, keys::TransparentKeyScope},
zcash_keys::encoding::TransparentCodecError,
};
@ -138,6 +139,10 @@ pub enum SqliteClientError {
/// linkability. The returned value contains the string encoding of the address and the txid(s)
/// of the transactions in which it is known to have been used.
AddressReuse(String, NonEmpty<TxId>),
/// The wallet encountered an error when attempting to schedule wallet operations.
#[cfg(feature = "transparent-inputs")]
Scheduling(SchedulingError),
}
impl error::Error for SqliteClientError {
@ -214,6 +219,10 @@ impl fmt::Display for SqliteClientError {
SqliteClientError::AddressReuse(address_str, txids) => {
write!(f, "The address {address_str} previously used in txid(s) {:?} would be reused.", txids)
}
#[cfg(feature = "transparent-inputs")]
SqliteClientError::Scheduling(err) => {
write!(f, "The wallet was unable to schedule an event: {}", err)
}
}
}
}
@ -278,3 +287,10 @@ impl From<AddressGenerationError> for SqliteClientError {
SqliteClientError::AddressGeneration(e)
}
}
#[cfg(feature = "transparent-inputs")]
impl From<SchedulingError> for SqliteClientError {
fn from(value: SchedulingError) -> Self {
SqliteClientError::Scheduling(value)
}
}

View File

@ -99,6 +99,7 @@ use {
#[cfg(feature = "transparent-inputs")]
use {
crate::wallet::transparent::ephemeral::schedule_ephemeral_address_checks,
::transparent::{address::TransparentAddress, bundle::OutPoint, keys::NonHardenedChildIndex},
std::collections::BTreeSet,
zcash_client_backend::wallet::TransparentAddressMetadata,
@ -361,10 +362,11 @@ impl From<zcash_client_backend::data_api::testing::transparent::GapLimits> for G
/// A wrapper for the SQLite connection to the wallet database, along with a capability to read the
/// system from the clock. A `WalletDb` encapsulates the full set of capabilities that are required
/// in order to implement the [`WalletRead`], [`WalletWrite`] and [`WalletCommitmentTrees`] traits.
pub struct WalletDb<C, P, CL> {
pub struct WalletDb<C, P, CL, R> {
conn: C,
params: P,
clock: CL,
rng: R,
#[cfg(feature = "transparent-inputs")]
gap_limits: GapLimits,
}
@ -378,7 +380,7 @@ impl Borrow<rusqlite::Connection> for SqlTransaction<'_> {
}
}
impl<P, CL> WalletDb<Connection, P, CL> {
impl<P, CL, R> WalletDb<Connection, P, CL, R> {
/// Construct a [`WalletDb`] instance that connects to the wallet database stored at the
/// specified path.
///
@ -386,10 +388,13 @@ impl<P, CL> WalletDb<Connection, P, CL> {
/// - `path`: The path to the SQLite database used to store wallet data.
/// - `params`: Parameters associated with the Zcash network that the wallet will connect to.
/// - `clock`: The clock to use in the case that the backend needs access to the system time.
/// - `rng`: The random number generation capability to be exposed by the created `WalletDb`
/// instance.
pub fn for_path<F: AsRef<Path>>(
path: F,
params: P,
clock: CL,
rng: R,
) -> Result<Self, rusqlite::Error> {
Connection::open(path).and_then(move |conn| {
rusqlite::vtab::array::load_module(&conn)?;
@ -397,6 +402,7 @@ impl<P, CL> WalletDb<Connection, P, CL> {
conn,
params,
clock,
rng,
#[cfg(feature = "transparent-inputs")]
gap_limits: GapLimits::default(),
})
@ -405,7 +411,7 @@ impl<P, CL> WalletDb<Connection, P, CL> {
}
#[cfg(feature = "transparent-inputs")]
impl<C, P, CL> WalletDb<C, P, CL> {
impl<C, P, CL, R> WalletDb<C, P, CL, R> {
/// Sets the gap limits to be used by the wallet in transparent address generation.
pub fn with_gap_limits(mut self, gap_limits: GapLimits) -> Self {
self.gap_limits = gap_limits;
@ -413,7 +419,7 @@ impl<C, P, CL> WalletDb<C, P, CL> {
}
}
impl<C: Borrow<rusqlite::Connection>, P, CL> WalletDb<C, P, CL> {
impl<C: Borrow<rusqlite::Connection>, P, CL, R> WalletDb<C, P, CL, R> {
/// Constructs a new wrapper around the given connection.
///
/// This is provided for use cases such as connection pooling, where `conn` may be an
@ -426,27 +432,31 @@ impl<C: Borrow<rusqlite::Connection>, P, CL> WalletDb<C, P, CL> {
/// - `conn`: A connection to the wallet database.
/// - `params`: Parameters associated with the Zcash network that the wallet will connect to.
/// - `clock`: The clock to use in the case that the backend needs access to the system time.
pub fn from_connection(conn: C, params: P, clock: CL) -> Self {
/// - `rng`: The random number generation capability to be exposed by the created `WalletDb`
/// instance.
pub fn from_connection(conn: C, params: P, clock: CL, rng: R) -> Self {
WalletDb {
conn,
params,
clock,
rng,
#[cfg(feature = "transparent-inputs")]
gap_limits: GapLimits::default(),
}
}
}
impl<C: BorrowMut<Connection>, P, CL> WalletDb<C, P, CL> {
impl<C: BorrowMut<Connection>, P, CL, R> WalletDb<C, P, CL, R> {
pub fn transactionally<F, A, E: From<rusqlite::Error>>(&mut self, f: F) -> Result<A, E>
where
F: FnOnce(&mut WalletDb<SqlTransaction<'_>, &P, &CL>) -> Result<A, E>,
F: FnOnce(&mut WalletDb<SqlTransaction<'_>, &P, &CL, &mut R>) -> Result<A, E>,
{
let tx = self.conn.borrow_mut().transaction()?;
let mut wdb = WalletDb {
conn: SqlTransaction(&tx),
params: &self.params,
clock: &self.clock,
rng: &mut self.rng,
#[cfg(feature = "transparent-inputs")]
gap_limits: self.gap_limits,
};
@ -497,8 +507,24 @@ impl<C: BorrowMut<Connection>, P, CL> WalletDb<C, P, CL> {
}
}
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL> InputSource
for WalletDb<C, P, CL>
#[cfg(feature = "transparent-inputs")]
impl<C: BorrowMut<Connection>, P, CL: Clock, R: rand::RngCore> WalletDb<C, P, CL, R> {
/// For each ephemeral address in the wallet, ensure that the transaction data request queue
/// contains a request for the wallet to check for UTXOs belonging to that address at some time
/// during the next 24-hour period.
///
/// We use randomized scheduling of ephemeral address checks to ensure that a
/// lightwalletd-compromising adversary cannot use temporal clustering to determine what
/// ephemeral addresses belong to a given wallet.
pub fn schedule_ephemeral_address_checks(&mut self) -> Result<(), SqliteClientError> {
self.borrow_mut().transactionally(|wdb| {
schedule_ephemeral_address_checks(wdb.conn.0, wdb.clock, &mut wdb.rng)
})
}
}
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL, R> InputSource
for WalletDb<C, P, CL, R>
{
type Error = SqliteClientError;
type NoteRef = ReceivedNoteId;
@ -630,8 +656,8 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL> InputSource
}
}
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL> WalletRead
for WalletDb<C, P, CL>
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL, R> WalletRead
for WalletDb<C, P, CL, R>
{
type Error = SqliteClientError;
type AccountId = AccountUuid;
@ -940,8 +966,8 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL> WalletRead
}
#[cfg(any(test, feature = "test-dependencies"))]
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL> WalletTest
for WalletDb<C, P, CL>
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL, R> WalletTest
for WalletDb<C, P, CL, R>
{
fn get_tx_history(
&self,
@ -1078,8 +1104,8 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters, CL> WalletTest
}
}
impl<C: BorrowMut<rusqlite::Connection>, P: consensus::Parameters, CL: Clock> WalletWrite
for WalletDb<C, P, CL>
impl<C: BorrowMut<rusqlite::Connection>, P: consensus::Parameters, CL: Clock, R> WalletWrite
for WalletDb<C, P, CL, R>
{
type UtxoRef = UtxoId;
@ -1773,7 +1799,7 @@ impl<C: BorrowMut<rusqlite::Connection>, P: consensus::Parameters, CL: Clock> Wa
) -> Result<(), Self::Error> {
self.transactionally(|wdb| {
for sent_tx in transactions {
wallet::store_transaction_to_be_sent(wdb, sent_tx)?;
wallet::store_transaction_to_be_sent(wdb.conn.0, &wdb.params, sent_tx)?;
}
Ok(())
})
@ -1864,8 +1890,8 @@ where
))
}
impl<C: BorrowMut<rusqlite::Connection>, P: consensus::Parameters, CL> WalletCommitmentTrees
for WalletDb<C, P, CL>
impl<C: BorrowMut<rusqlite::Connection>, P: consensus::Parameters, CL, R> WalletCommitmentTrees
for WalletDb<C, P, CL, R>
{
type Error = commitment_tree::Error;
type SaplingShardStore<'a> = SaplingShardStore<&'a rusqlite::Transaction<'a>>;
@ -1964,7 +1990,9 @@ impl<C: BorrowMut<rusqlite::Connection>, P: consensus::Parameters, CL> WalletCom
}
}
impl<P: consensus::Parameters, CL> WalletCommitmentTrees for WalletDb<SqlTransaction<'_>, P, CL> {
impl<P: consensus::Parameters, CL, R> WalletCommitmentTrees
for WalletDb<SqlTransaction<'_>, P, CL, R>
{
type Error = commitment_tree::Error;
type SaplingShardStore<'a> = crate::SaplingShardStore<&'a rusqlite::Transaction<'a>>;
@ -2665,10 +2693,14 @@ mod tests {
#[cfg(feature = "transparent-inputs")]
#[test]
fn transparent_receivers() {
// Add an account to the wallet.
use std::collections::BTreeSet;
use crate::testing::BlockCache;
let st = TestBuilder::new()
use crate::{
testing::BlockCache, wallet::transparent::transaction_data_requests, GapLimits,
};
use zcash_client_backend::data_api::TransactionDataRequest;
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory::default())
.with_block_cache(BlockCache::new())
.with_account_from_sapling_activation(BlockHash([0; 32]))
@ -2676,6 +2708,8 @@ mod tests {
let account = st.test_account().unwrap();
let ufvk = account.usk().to_unified_full_viewing_key();
let (taddr, _) = account.usk().default_transparent_address();
let birthday = account.birthday().height();
let account_id = account.id();
let receivers = st
.wallet()
@ -2693,6 +2727,53 @@ mod tests {
// The default t-addr should be in the set.
assert!(receivers.contains_key(&taddr));
// The chain tip height must be known in order to query for data requests.
st.wallet_mut().update_chain_tip(birthday).unwrap();
// Transaction data requests should include a request for each ephemeral address
let ephemeral_addrs = st
.wallet()
.get_known_ephemeral_addresses(account_id, None)
.unwrap();
assert_eq!(
ephemeral_addrs.len(),
GapLimits::default().ephemeral() as usize
);
st.wallet_mut()
.db_mut()
.schedule_ephemeral_address_checks()
.unwrap();
let data_requests =
transaction_data_requests(st.wallet().conn(), &st.wallet().db().params).unwrap();
let base_time = st.wallet().db().clock.now();
let day = Duration::from_secs(60 * 60 * 24);
let mut check_times = BTreeSet::new();
for (addr, _) in ephemeral_addrs {
let has_valid_request = data_requests.iter().any(|req| match req {
TransactionDataRequest::TransactionsInvolvingAddress {
address,
request_at: Some(t),
..
} => {
*address == addr && *t > base_time && {
let t_delta = t.duration_since(base_time).unwrap();
// This is an imprecise check; the objective of the randomized time
// selection is that all ephemeral address checks be performed within a
// day, and that their check times be distinct.
let result = t_delta < day && !check_times.contains(t);
check_times.insert(*t);
result
}
}
_ => false,
});
assert!(has_valid_request);
}
}
#[cfg(feature = "unstable")]

View File

@ -1,4 +1,6 @@
use ambassador::Delegate;
use rand::SeedableRng;
use rand_chacha::ChaChaRng;
use rusqlite::Connection;
use std::num::NonZeroU32;
use std::time::Duration;
@ -34,10 +36,9 @@ use zcash_protocol::{
};
use zip32::{fingerprint::SeedFingerprint, DiversifierIndex};
use crate::wallet::init::init_wallet_db_internal;
use crate::{
error::SqliteClientError,
util::testing::FixedClock,
wallet::init::{init_wallet_db, init_wallet_db_internal},
error::SqliteClientError, util::testing::FixedClock, wallet::init::testing::init_wallet_db,
AccountUuid, WalletDb,
};
@ -56,6 +57,10 @@ pub(crate) fn test_clock() -> FixedClock {
FixedClock::new(SystemTime::UNIX_EPOCH + TEST_EPOCH_SECONDS_OFFSET)
}
pub(crate) fn test_rng() -> ChaChaRng {
ChaChaRng::from_seed([0u8; 32])
}
#[allow(clippy::duplicated_attributes, reason = "False positive")]
#[derive(Delegate)]
#[delegate(InputSource, target = "wallet_db")]
@ -64,13 +69,13 @@ pub(crate) fn test_clock() -> FixedClock {
#[delegate(WalletWrite, target = "wallet_db")]
#[delegate(WalletCommitmentTrees, target = "wallet_db")]
pub(crate) struct TestDb {
wallet_db: WalletDb<Connection, LocalNetwork, FixedClock>,
wallet_db: WalletDb<Connection, LocalNetwork, FixedClock, ChaChaRng>,
data_file: NamedTempFile,
}
impl TestDb {
fn from_parts(
wallet_db: WalletDb<Connection, LocalNetwork, FixedClock>,
wallet_db: WalletDb<Connection, LocalNetwork, FixedClock, ChaChaRng>,
data_file: NamedTempFile,
) -> Self {
Self {
@ -79,11 +84,13 @@ impl TestDb {
}
}
pub(crate) fn db(&self) -> &WalletDb<Connection, LocalNetwork, FixedClock> {
pub(crate) fn db(&self) -> &WalletDb<Connection, LocalNetwork, FixedClock, ChaChaRng> {
&self.wallet_db
}
pub(crate) fn db_mut(&mut self) -> &mut WalletDb<Connection, LocalNetwork, FixedClock> {
pub(crate) fn db_mut(
&mut self,
) -> &mut WalletDb<Connection, LocalNetwork, FixedClock, ChaChaRng> {
&mut self.wallet_db
}
@ -180,7 +187,8 @@ impl DataStoreFactory for TestDbFactory {
#[cfg(feature = "transparent-inputs")] gap_limits: GapLimits,
) -> Result<Self::DataStore, Self::Error> {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), network, test_clock()).unwrap();
let mut db_data =
WalletDb::for_path(data_file.path(), network, test_clock(), test_rng()).unwrap();
#[cfg(feature = "transparent-inputs")]
{
db_data = db_data.with_gap_limits(gap_limits.into());

View File

@ -2587,12 +2587,13 @@ pub(crate) fn get_max_height_hash(
.optional()
}
pub(crate) fn store_transaction_to_be_sent<P: consensus::Parameters, CL>(
wdb: &mut WalletDb<SqlTransaction<'_>, P, CL>,
pub(crate) fn store_transaction_to_be_sent<P: consensus::Parameters>(
conn: &rusqlite::Transaction,
params: &P,
sent_tx: &SentTransaction<AccountUuid>,
) -> Result<(), SqliteClientError> {
let tx_ref = put_tx_data(
wdb.conn.0,
conn,
sent_tx.tx(),
Some(sent_tx.fee_amount()),
Some(sent_tx.created()),
@ -2612,7 +2613,7 @@ pub(crate) fn store_transaction_to_be_sent<P: consensus::Parameters, CL>(
if let Some(bundle) = sent_tx.tx().sapling_bundle() {
detectable_via_scanning = true;
for spend in bundle.shielded_spends() {
sapling::mark_sapling_note_spent(wdb.conn.0, tx_ref, spend.nullifier())?;
sapling::mark_sapling_note_spent(conn, tx_ref, spend.nullifier())?;
}
}
if let Some(_bundle) = sent_tx.tx().orchard_bundle() {
@ -2620,7 +2621,7 @@ pub(crate) fn store_transaction_to_be_sent<P: consensus::Parameters, CL>(
{
detectable_via_scanning = true;
for action in _bundle.actions() {
orchard::mark_orchard_note_spent(wdb.conn.0, tx_ref, action.nullifier())?;
orchard::mark_orchard_note_spent(conn, tx_ref, action.nullifier())?;
}
}
@ -2630,17 +2631,11 @@ pub(crate) fn store_transaction_to_be_sent<P: consensus::Parameters, CL>(
#[cfg(feature = "transparent-inputs")]
for utxo_outpoint in sent_tx.utxos_spent() {
transparent::mark_transparent_utxo_spent(wdb.conn.0, tx_ref, utxo_outpoint)?;
transparent::mark_transparent_utxo_spent(conn, tx_ref, utxo_outpoint)?;
}
for output in sent_tx.outputs() {
insert_sent_output(
wdb.conn.0,
&wdb.params,
tx_ref,
*sent_tx.account_id(),
output,
)?;
insert_sent_output(conn, params, tx_ref, *sent_tx.account_id(), output)?;
match output.recipient() {
Recipient::External {
@ -2657,8 +2652,8 @@ pub(crate) fn store_transaction_to_be_sent<P: consensus::Parameters, CL>(
} => match note.as_ref() {
Note::Sapling(note) => {
sapling::put_received_note(
wdb.conn.0,
&wdb.params,
conn,
params,
&DecryptedOutput::new(
output.output_index(),
note.clone(),
@ -2676,8 +2671,8 @@ pub(crate) fn store_transaction_to_be_sent<P: consensus::Parameters, CL>(
#[cfg(feature = "orchard")]
Note::Orchard(note) => {
orchard::put_received_note(
wdb.conn.0,
&wdb.params,
conn,
params,
&DecryptedOutput::new(
output.output_index(),
*note,
@ -2701,15 +2696,11 @@ pub(crate) fn store_transaction_to_be_sent<P: consensus::Parameters, CL>(
} => {
// First check to verify that creation of this output does not result in reuse of
// an ephemeral address.
transparent::check_ephemeral_address_reuse(
wdb.conn.0,
&wdb.params,
ephemeral_address,
)?;
transparent::check_ephemeral_address_reuse(conn, params, ephemeral_address)?;
transparent::put_transparent_output(
wdb.conn.0,
&wdb.params,
conn,
&params,
outpoint,
&TxOut {
value: output.value(),
@ -2727,7 +2718,7 @@ pub(crate) fn store_transaction_to_be_sent<P: consensus::Parameters, CL>(
// at present for fully transparent transactions, because any transaction with a shielded
// component will be detected via ordinary chain scanning and/or nullifier checking.
if !detectable_via_scanning {
queue_tx_retrieval(wdb.conn.0, std::iter::once(sent_tx.tx().txid()), None)?;
queue_tx_retrieval(conn, std::iter::once(sent_tx.tx().txid()), None)?;
}
Ok(())
@ -2930,6 +2921,7 @@ pub(crate) fn truncate_to_height<P: consensus::Parameters>(
conn: SqlTransaction(conn),
params: params.clone(),
clock: (),
rng: (),
#[cfg(feature = "transparent-inputs")]
gap_limits: *gap_limits,
};

View File

@ -1190,13 +1190,26 @@ mod tests {
use zcash_protocol::consensus::{BlockHeight, Network};
use super::SqliteShardStore;
use crate::{testing::pool::ShieldedPoolPersistence, wallet::init::init_wallet_db, WalletDb};
use crate::{
testing::{
db::{test_clock, test_rng},
pool::ShieldedPoolPersistence,
},
wallet::init::testing::init_wallet_db,
WalletDb,
};
fn new_tree<T: ShieldedPoolTester + ShieldedPoolPersistence>(
m: usize,
) -> ShardTree<SqliteShardStore<rusqlite::Connection, String, 3>, 4, 3> {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork, ()).unwrap();
let mut db_data = WalletDb::for_path(
data_file.path(),
Network::TestNetwork,
test_clock(),
test_rng(),
)
.unwrap();
data_file.keep().unwrap();
init_wallet_db(&mut db_data, None).unwrap();
@ -1294,7 +1307,13 @@ mod tests {
fn put_shard_roots<T: ShieldedPoolTester + ShieldedPoolPersistence>() {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork, ()).unwrap();
let mut db_data = WalletDb::for_path(
data_file.path(),
Network::TestNetwork,
test_clock(),
test_rng(),
)
.unwrap();
data_file.keep().unwrap();
init_wallet_db(&mut db_data, None).unwrap();

View File

@ -101,6 +101,9 @@ pub(super) const INDEX_HD_ACCOUNT: &str =
/// cases, this is what user-generated addresses will be assigned.
/// - `receiver_flags`: A set of bitflags that describes which receiver types are included in
/// `address`. See the documentation of [`ReceiverFlags`] for details.
/// - `transparent_receiver_next_check_time`: The Unix epoch time at which a client should next
/// check to determine whether any new UTXOs have been received by the cached transparent receiver
/// address. At present, this will ordinarily be populated only for ZIP 320 ephemeral addresses.
///
/// [`ReceiverFlags`]: crate::wallet::encoding::ReceiverFlags
pub(super) const TABLE_ADDRESSES: &str = r#"
@ -114,6 +117,7 @@ CREATE TABLE "addresses" (
cached_transparent_receiver_address TEXT,
exposed_at_height INTEGER,
receiver_flags INTEGER NOT NULL,
transparent_receiver_next_check_time INTEGER,
FOREIGN KEY (account_id) REFERENCES accounts(id),
CONSTRAINT diversification UNIQUE (account_id, key_scope, diversifier_index_be),
CONSTRAINT transparent_index_consistency CHECK (

View File

@ -18,7 +18,10 @@ use zip32::DiversifierIndex;
use crate::error::SqliteClientError;
#[cfg(feature = "transparent-inputs")]
use transparent::keys::TransparentKeyScope;
use {
super::transparent::SchedulingError, std::time::SystemTime,
transparent::keys::TransparentKeyScope,
};
pub(crate) fn pool_code(pool_type: PoolType) -> i64 {
// These constants are *incidentally* shared with the typecodes
@ -66,6 +69,21 @@ pub(crate) fn memo_repr(memo: Option<&MemoBytes>) -> Option<&[u8]> {
})
}
#[cfg(feature = "transparent-inputs")]
pub(crate) fn epoch_seconds(t: SystemTime) -> Result<i64, SchedulingError> {
let integer_seconds_since_epoch =
i64::try_from(t.duration_since(SystemTime::UNIX_EPOCH)?.as_secs())?;
Ok(integer_seconds_since_epoch)
}
#[cfg(feature = "transparent-inputs")]
pub(crate) fn decode_epoch_seconds(i: i64) -> Result<SystemTime, SchedulingError> {
use std::time::Duration;
Ok(SystemTime::UNIX_EPOCH + Duration::from_secs(u64::try_from(i)?))
}
/// An enumeration of the scopes of keys that are generated by the `zcash_client_sqlite`
/// implementation of the `WalletWrite` trait.
///

View File

@ -4,6 +4,7 @@ use std::borrow::BorrowMut;
use std::fmt;
use std::rc::Rc;
use rand_core::RngCore;
use regex::Regex;
use schemerz::{Migrator, MigratorError};
use schemerz_rusqlite::RusqliteAdapter;
@ -18,7 +19,7 @@ use zcash_protocol::{consensus, value::BalanceError};
use self::migrations::verify_network_compatibility;
use super::commitment_tree;
use crate::{error::SqliteClientError, WalletDb};
use crate::{error::SqliteClientError, util::Clock, WalletDb};
mod migrations;
@ -236,6 +237,10 @@ fn sqlite_client_error_to_wallet_migration_error(e: SqliteClientError) -> Wallet
SqliteClientError::NoteFilterInvalid(_) => {
unreachable!("we don't do note selection in migrations")
}
#[cfg(feature = "transparent-inputs")]
SqliteClientError::Scheduling(e) => {
WalletMigrationError::Other(SqliteClientError::Scheduling(e))
}
}
}
@ -283,6 +288,7 @@ fn sqlite_client_error_to_wallet_migration_error(e: SqliteClientError) -> Wallet
/// # use std::error::Error;
/// # use secrecy::SecretVec;
/// # use tempfile::NamedTempFile;
/// use rand_core::OsRng;
/// use zcash_protocol::consensus::Network;
/// use zcash_client_sqlite::{
/// WalletDb,
@ -294,7 +300,7 @@ fn sqlite_client_error_to_wallet_migration_error(e: SqliteClientError) -> Wallet
/// # let data_file = NamedTempFile::new().unwrap();
/// # let get_data_db_path = || data_file.path();
/// # let load_seed = || -> Result<_, String> { Ok(SecretVec::new(vec![])) };
/// let mut db = WalletDb::for_path(get_data_db_path(), Network::TestNetwork, SystemClock)?;
/// let mut db = WalletDb::for_path(get_data_db_path(), Network::TestNetwork, SystemClock, OsRng)?;
/// match init_wallet_db(&mut db, None) {
/// Err(e)
/// if matches!(
@ -319,9 +325,10 @@ fn sqlite_client_error_to_wallet_migration_error(e: SqliteClientError) -> Wallet
pub fn init_wallet_db<
C: BorrowMut<rusqlite::Connection>,
P: consensus::Parameters + 'static,
CL,
CL: Clock + Clone + 'static,
R: RngCore + Clone + 'static,
>(
wdb: &mut WalletDb<C, P, CL>,
wdb: &mut WalletDb<C, P, CL, R>,
seed: Option<SecretVec<u8>>,
) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
init_wallet_db_internal(wdb, seed, &[], true)
@ -330,9 +337,10 @@ pub fn init_wallet_db<
pub(crate) fn init_wallet_db_internal<
C: BorrowMut<rusqlite::Connection>,
P: consensus::Parameters + 'static,
CL,
CL: Clock + Clone + 'static,
R: RngCore + Clone + 'static,
>(
wdb: &mut WalletDb<C, P, CL>,
wdb: &mut WalletDb<C, P, CL, R>,
seed: Option<SecretVec<u8>>,
target_migrations: &[Uuid],
verify_seed_relevance: bool,
@ -374,7 +382,15 @@ pub(crate) fn init_wallet_db_internal<
let adapter = RusqliteAdapter::new(wdb.conn.borrow_mut(), Some(MIGRATIONS_TABLE.to_string()));
let mut migrator = Migrator::new(adapter);
migrator
.register_multiple(migrations::all_migrations(&wdb.params, seed.clone()).into_iter())
.register_multiple(
migrations::all_migrations(
&wdb.params,
wdb.clock.clone(),
wdb.rng.clone(),
seed.clone(),
)
.into_iter(),
)
.expect("Wallet migration registration should have been successful.");
if target_migrations.is_empty() {
migrator.up(None)?;
@ -448,8 +464,33 @@ fn verify_sqlite_version_compatibility(
}
}
#[cfg(test)]
pub(crate) mod testing {
use rand::RngCore;
use schemerz::MigratorError;
use secrecy::SecretVec;
use uuid::Uuid;
use zcash_protocol::consensus;
use crate::{util::Clock, WalletDb};
use super::WalletMigrationError;
pub(crate) fn init_wallet_db<
P: consensus::Parameters + 'static,
CL: Clock + Clone + 'static,
R: RngCore + Clone + 'static,
>(
wdb: &mut WalletDb<rusqlite::Connection, P, CL, R>,
seed: Option<SecretVec<u8>>,
) -> Result<(), MigratorError<Uuid, WalletMigrationError>> {
super::init_wallet_db_internal(wdb, seed, &[], true)
}
}
#[cfg(test)]
mod tests {
use rand::RngCore;
use rusqlite::{self, named_params, Connection, ToSql};
use secrecy::Secret;
@ -469,17 +510,18 @@ mod tests {
use zcash_protocol::consensus::{self, BlockHeight, BranchId, Network, NetworkConstants};
use zip32::AccountId;
use crate::{testing::db::TestDbFactory, wallet::db, WalletDb, UA_TRANSPARENT};
use super::init_wallet_db;
use super::testing::init_wallet_db;
use crate::{
testing::db::{test_clock, test_rng, TestDbFactory},
util::Clock,
wallet::db,
WalletDb, UA_TRANSPARENT,
};
#[cfg(feature = "transparent-inputs")]
use {
super::WalletMigrationError,
crate::{
testing::db::test_clock,
wallet::{self, pool_code, PoolType},
},
crate::wallet::{self, pool_code, PoolType},
zcash_address::test_vectors,
zcash_client_backend::data_api::WalletWrite,
zip32::DiversifierIndex,
@ -612,8 +654,8 @@ mod tests {
#[test]
fn init_migrate_from_0_3_0() {
fn init_0_3_0<P: consensus::Parameters, CL>(
wdb: &mut WalletDb<rusqlite::Connection, P, CL>,
fn init_0_3_0<P: consensus::Parameters, CL: Clock + Clone, R: RngCore + Clone>(
wdb: &mut WalletDb<rusqlite::Connection, P, CL, R>,
extfvk: &ExtendedFullViewingKey,
account: AccountId,
) -> Result<(), rusqlite::Error> {
@ -717,7 +759,13 @@ mod tests {
}
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork, ()).unwrap();
let mut db_data = WalletDb::for_path(
data_file.path(),
Network::TestNetwork,
test_clock(),
test_rng(),
)
.unwrap();
let seed = [0xab; 32];
let account = AccountId::ZERO;
@ -734,8 +782,8 @@ mod tests {
#[test]
fn init_migrate_from_autoshielding_poc() {
fn init_autoshielding<P: consensus::Parameters, CL>(
wdb: &mut WalletDb<rusqlite::Connection, P, CL>,
fn init_autoshielding<P: consensus::Parameters, CL, R>(
wdb: &mut WalletDb<rusqlite::Connection, P, CL, R>,
extfvk: &ExtendedFullViewingKey,
account: AccountId,
) -> Result<(), rusqlite::Error> {
@ -889,7 +937,13 @@ mod tests {
}
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork, ()).unwrap();
let mut db_data = WalletDb::for_path(
data_file.path(),
Network::TestNetwork,
test_clock(),
test_rng(),
)
.unwrap();
let seed = [0xab; 32];
let account = AccountId::ZERO;
@ -906,8 +960,8 @@ mod tests {
#[test]
fn init_migrate_from_main_pre_migrations() {
fn init_main<P: consensus::Parameters, CL>(
wdb: &mut WalletDb<rusqlite::Connection, P, CL>,
fn init_main<P: consensus::Parameters, CL, R>(
wdb: &mut WalletDb<rusqlite::Connection, P, CL, R>,
ufvk: &UnifiedFullViewingKey,
account: AccountId,
) -> Result<(), rusqlite::Error> {
@ -1057,7 +1111,13 @@ mod tests {
}
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork, ()).unwrap();
let mut db_data = WalletDb::for_path(
data_file.path(),
Network::TestNetwork,
test_clock(),
test_rng(),
)
.unwrap();
let seed = [0xab; 32];
let account = AccountId::ZERO;
@ -1083,7 +1143,8 @@ mod tests {
let network = Network::MainNetwork;
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), network, test_clock()).unwrap();
let mut db_data =
WalletDb::for_path(data_file.path(), network, test_clock(), test_rng()).unwrap();
assert_matches!(init_wallet_db(&mut db_data, None), Ok(_));
// Prior to adding any accounts, every seed phrase is relevant to the wallet.

View File

@ -33,8 +33,9 @@ mod v_transactions_transparent_history;
mod v_tx_outputs_use_legacy_false;
mod wallet_summaries;
use std::rc::Rc;
use std::{rc::Rc, sync::Mutex};
use rand_core::RngCore;
use rusqlite::{named_params, OptionalExtension};
use schemerz_rusqlite::RusqliteMigration;
use secrecy::SecretVec;
@ -42,10 +43,18 @@ use uuid::Uuid;
use zcash_address::unified::{Encoding as _, Ufvk};
use zcash_protocol::consensus;
use crate::util::Clock;
use super::WalletMigrationError;
pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
pub(super) fn all_migrations<
P: consensus::Parameters + 'static,
C: Clock + Clone + 'static,
R: RngCore + Clone + 'static,
>(
params: &P,
clock: C,
rng: R,
seed: Option<Rc<SecretVec<u8>>>,
) -> Vec<Box<dyn RusqliteMigration<Error = WalletMigrationError>>> {
// initial_setup
@ -90,6 +99,7 @@ pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
// fix_bad_change_flagging v_transactions_additional_totals
// |
// transparent_gap_limit_handling
let rng = Rc::new(Mutex::new(rng));
vec![
Box::new(initial_setup::Migration {}),
Box::new(utxos_table::Migration {}),
@ -156,6 +166,8 @@ pub(super) fn all_migrations<P: consensus::Parameters + 'static>(
Box::new(v_transactions_additional_totals::Migration),
Box::new(transparent_gap_limit_handling::Migration {
params: params.clone(),
_clock: clock.clone(),
_rng: rng.clone(),
}),
]
}
@ -296,13 +308,23 @@ mod tests {
use uuid::Uuid;
use zcash_protocol::consensus::Network;
use crate::{wallet::init::init_wallet_db_internal, WalletDb};
use crate::{
testing::db::{test_clock, test_rng},
wallet::init::init_wallet_db_internal,
WalletDb,
};
/// Tests that we can migrate from a completely empty wallet database to the target
/// migrations.
pub(crate) fn test_migrate(migrations: &[Uuid]) {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork, ()).unwrap();
let mut db_data = WalletDb::for_path(
data_file.path(),
Network::TestNetwork,
test_clock(),
test_rng(),
)
.unwrap();
let seed = [0xab; 32];
assert_matches!(
@ -319,7 +341,13 @@ mod tests {
#[test]
fn migrate_between_releases_without_data() {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork, ()).unwrap();
let mut db_data = WalletDb::for_path(
data_file.path(),
Network::TestNetwork,
test_clock(),
test_rng(),
)
.unwrap();
let seed = [0xab; 32].to_vec();

View File

@ -83,13 +83,18 @@ mod tests {
use zip32::AccountId;
use super::{DEPENDENCIES, MIGRATION_ID};
use crate::{wallet::init::init_wallet_db_internal, WalletDb};
use crate::{
testing::db::{test_clock, test_rng},
wallet::init::init_wallet_db_internal,
WalletDb,
};
#[test]
fn migrate() {
let data_file = NamedTempFile::new().unwrap();
let network = Network::TestNetwork;
let mut db_data = WalletDb::for_path(data_file.path(), network, ()).unwrap();
let mut db_data =
WalletDb::for_path(data_file.path(), network, test_clock(), test_rng()).unwrap();
let seed_bytes = vec![0xab; 32];
init_wallet_db_internal(

View File

@ -278,6 +278,7 @@ mod tests {
use zip32::AccountId;
use crate::{
testing::db::{test_clock, test_rng},
wallet::init::{init_wallet_db_internal, migrations::addresses_table},
WalletDb,
};
@ -302,7 +303,8 @@ mod tests {
fn transaction_views() {
let network = Network::TestNetwork;
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), network, ()).unwrap();
let mut db_data =
WalletDb::for_path(data_file.path(), network, test_clock(), test_rng()).unwrap();
init_wallet_db_internal(&mut db_data, None, &[addresses_table::MIGRATION_ID], false)
.unwrap();
let usk = UnifiedSpendingKey::from_seed(&network, &[0u8; 32][..], AccountId::ZERO).unwrap();
@ -401,7 +403,8 @@ mod tests {
let network = Network::TestNetwork;
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), network, ()).unwrap();
let mut db_data =
WalletDb::for_path(data_file.path(), network, test_clock(), test_rng()).unwrap();
init_wallet_db_internal(
&mut db_data,
None,

View File

@ -99,14 +99,23 @@ mod tests {
use zcash_protocol::consensus::Network;
use crate::{
wallet::init::{init_wallet_db, init_wallet_db_internal, migrations::addresses_table},
testing::db::{test_clock, test_rng},
wallet::init::{
init_wallet_db_internal, migrations::addresses_table, testing::init_wallet_db,
},
WalletDb, UA_ORCHARD, UA_TRANSPARENT,
};
#[test]
fn init_migrate_add_orchard_receiver() {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork, ()).unwrap();
let mut db_data = WalletDb::for_path(
data_file.path(),
Network::TestNetwork,
test_clock(),
test_rng(),
)
.unwrap();
let seed = vec![0x10; 32];
let account_id = 0u32;

View File

@ -231,6 +231,7 @@ mod tests {
use zip32::AccountId;
use crate::{
testing::db::{test_clock, test_rng},
wallet::init::{init_wallet_db_internal, migrations::v_transactions_net},
WalletDb,
};
@ -238,7 +239,13 @@ mod tests {
#[test]
fn received_notes_nullable_migration() {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork, ()).unwrap();
let mut db_data = WalletDb::for_path(
data_file.path(),
Network::TestNetwork,
test_clock(),
test_rng(),
)
.unwrap();
init_wallet_db_internal(
&mut db_data,
None,

View File

@ -321,6 +321,7 @@ mod tests {
use crate::{
error::SqliteClientError,
testing::db::{test_clock, test_rng},
wallet::{
init::{
init_wallet_db_internal,
@ -337,8 +338,8 @@ mod tests {
const EXTERNAL_VALUE: u64 = 10;
const INTERNAL_VALUE: u64 = 5;
fn prepare_wallet_state<P: Parameters, CL>(
db_data: &mut WalletDb<Connection, P, CL>,
fn prepare_wallet_state<P: Parameters, CL, R>(
db_data: &mut WalletDb<Connection, P, CL, R>,
) -> (UnifiedFullViewingKey, BlockHeight, BuildResult) {
// Create an account in the wallet
let usk0 =
@ -527,7 +528,8 @@ mod tests {
// Create wallet upgraded to just before the current migration.
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), params, ()).unwrap();
let mut db_data =
WalletDb::for_path(data_file.path(), params, test_clock(), test_rng()).unwrap();
init_wallet_db_internal(
&mut db_data,
None,
@ -626,7 +628,8 @@ mod tests {
// Create wallet upgraded to just before the current migration.
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), params, ()).unwrap();
let mut db_data =
WalletDb::for_path(data_file.path(), params, test_clock(), test_rng()).unwrap();
init_wallet_db_internal(
&mut db_data,
None,

View File

@ -1,7 +1,10 @@
//! Add support for general transparent gap limit handling, and unify the `addresses` and
//! `ephemeral_addresses` tables.
use rand_core::RngCore;
use std::collections::HashSet;
use std::rc::Rc;
use std::sync::Mutex;
use uuid::Uuid;
use rusqlite::{named_params, Transaction};
@ -13,6 +16,7 @@ use zcash_protocol::consensus::{self, BlockHeight};
use super::add_account_uuids;
use crate::{
util::Clock,
wallet::{self, encoding::ReceiverFlags, init::WalletMigrationError, KeyScope},
AccountRef,
};
@ -21,8 +25,8 @@ use crate::{
use {
crate::{
wallet::{
encoding::{decode_diversifier_index_be, encode_diversifier_index_be},
transparent::generate_gap_addresses,
encoding::{decode_diversifier_index_be, encode_diversifier_index_be, epoch_seconds},
transparent::{generate_gap_addresses, next_check_time},
},
GapLimits,
},
@ -36,11 +40,13 @@ pub(super) const MIGRATION_ID: Uuid = Uuid::from_u128(0xc41dfc0e_e870_4859_be47_
const DEPENDENCIES: &[Uuid] = &[add_account_uuids::MIGRATION_ID];
pub(super) struct Migration<P> {
pub(super) struct Migration<P, C, R> {
pub(super) params: P,
pub(super) _clock: C,
pub(super) _rng: Rc<Mutex<R>>,
}
impl<P> schemerz::Migration<Uuid> for Migration<P> {
impl<P, C, R> schemerz::Migration<Uuid> for Migration<P, C, R> {
fn id(&self) -> Uuid {
MIGRATION_ID
}
@ -54,7 +60,7 @@ impl<P> schemerz::Migration<Uuid> for Migration<P> {
}
}
impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
impl<P: consensus::Parameters, C: Clock, R: RngCore> RusqliteMigration for Migration<P, C, R> {
type Error = WalletMigrationError;
fn up(&self, conn: &Transaction) -> Result<(), WalletMigrationError> {
@ -128,7 +134,7 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
":account_id": account_id,
":diversifier_index_be": &di_be[..],
":account_birthday": account_birthday,
":receiver_flags": receiver_flags.bits()
":receiver_flags": receiver_flags.bits(),
},
)
};
@ -169,7 +175,7 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
":transparent_child_index": idx.index(),
":t_addr": t_addr,
":account_birthday": account_birthday,
":receiver_flags": receiver_flags.bits()
":receiver_flags": receiver_flags.bits(),
},
)?;
} else {
@ -182,7 +188,7 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
update_without_taddr()?;
}
}
}
};
// We now have to re-create the `addresses` table in order to fix the constraints. Note
// that we do not include the `seen_in_tx` column as this is duplicative of information
@ -201,6 +207,7 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
cached_transparent_receiver_address TEXT,
exposed_at_height INTEGER,
receiver_flags INTEGER NOT NULL,
transparent_receiver_next_check_time INTEGER,
FOREIGN KEY (account_id) REFERENCES accounts(id),
CONSTRAINT diversification UNIQUE (account_id, key_scope, diversifier_index_be),
CONSTRAINT transparent_index_consistency CHECK (
@ -208,6 +215,7 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
)
);
-- we will only set `transparent_receiver_next_check_time` for ephemeral addresses
INSERT INTO addresses_new (
account_id, key_scope, diversifier_index_be, address,
transparent_child_index, cached_transparent_receiver_address,
@ -228,11 +236,13 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
INSERT INTO addresses_new (
account_id, key_scope, diversifier_index_be, address,
transparent_child_index, cached_transparent_receiver_address,
exposed_at_height, receiver_flags
exposed_at_height, receiver_flags,
transparent_receiver_next_check_time
) VALUES (
:account_id, :key_scope, :diversifier_index_be, :address,
:transparent_child_index, :cached_transparent_receiver_address,
:exposed_at_height, :receiver_flags
:exposed_at_height, :receiver_flags,
:transparent_receiver_next_check_time
)
"#,
)?;
@ -246,22 +256,59 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
LEFT OUTER JOIN transactions t ON t.id_tx = ea.used_in_tx
"#,
)?;
let mut rows =
ea_query.query(named_params! {":expiry_delta": DEFAULT_TX_EXPIRY_DELTA })?;
while let Some(row) = rows.next()? {
let account_id: i64 = row.get("account_id")?;
let transparent_child_index = row.get::<_, i64>("address_index")?;
let diversifier_index = DiversifierIndex::from(
u32::try_from(transparent_child_index)
.ok()
.and_then(NonHardenedChildIndex::from_index)
.ok_or(WalletMigrationError::CorruptedData(
"ephermeral address indices must be in the range of `u31`".to_owned(),
))?
.index(),
);
let address: String = row.get("address")?;
let exposed_at_height: Option<i64> = row.get("exposed_at_height")?;
let rows = ea_query
.query_and_then(
named_params! {":expiry_delta": DEFAULT_TX_EXPIRY_DELTA },
|row| {
let account_id: i64 = row.get("account_id")?;
let transparent_child_index = row.get::<_, i64>("address_index")?;
let diversifier_index = DiversifierIndex::from(
u32::try_from(transparent_child_index)
.ok()
.and_then(NonHardenedChildIndex::from_index)
.ok_or(WalletMigrationError::CorruptedData(
"ephermeral address indices must be in the range of `u31`"
.to_owned(),
))?
.index(),
);
let address: String = row.get("address")?;
let exposed_at_height: Option<i64> = row.get("exposed_at_height")?;
Ok((
account_id,
diversifier_index,
transparent_child_index,
address,
exposed_at_height,
))
},
)?
.collect::<Result<Vec<_>, WalletMigrationError>>()?;
let ephemeral_address_count =
u32::try_from(rows.len()).expect("number of ephemeral addrs fits into u32");
let mut check_time = self._clock.now();
for (
account_id,
diversifier_index,
transparent_child_index,
address,
exposed_at_height,
) in rows
{
// Compute a next check time for the address such that, when considered in the
// context of all other allocated ephemeral addresses, it will be checked once per
// day.
let next_check_time = {
let rng = self
._rng
.lock()
.expect("can obtain write lock to shared rng");
next_check_time(rng, check_time, (24 * 60 * 60) / ephemeral_address_count)
.expect("computed next check time is valid")
};
let next_check_epoch_seconds = epoch_seconds(next_check_time).unwrap();
// We set both the `address` column and the `cached_transparent_receiver_address`
// column to the same value here; there is no Unified address that corresponds to
@ -274,10 +321,12 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
":transparent_child_index": transparent_child_index,
":cached_transparent_receiver_address": address,
":exposed_at_height": exposed_at_height,
":receiver_flags": ReceiverFlags::P2PKH.bits()
":receiver_flags": ReceiverFlags::P2PKH.bits(),
":transparent_receiver_next_check_time": next_check_epoch_seconds
})?;
account_ids.insert(account_id);
check_time = next_check_time;
}
}

View File

@ -234,6 +234,7 @@ mod tests {
};
use crate::{
testing::db::{test_clock, test_rng},
wallet::init::{init_wallet_db_internal, migrations::tests::test_migrate},
WalletDb,
};
@ -248,7 +249,13 @@ mod tests {
#[test]
fn migrate_with_data() {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork, ()).unwrap();
let mut db_data = WalletDb::for_path(
data_file.path(),
Network::TestNetwork,
test_clock(),
test_rng(),
)
.unwrap();
let seed_bytes = vec![0xab; 32];

View File

@ -212,6 +212,7 @@ mod tests {
use zip32::AccountId;
use crate::{
testing::db::{test_clock, test_rng},
wallet::init::{init_wallet_db_internal, migrations::add_transaction_views},
WalletDb,
};
@ -219,7 +220,13 @@ mod tests {
#[test]
fn v_transactions_net() {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork, ()).unwrap();
let mut db_data = WalletDb::for_path(
data_file.path(),
Network::TestNetwork,
test_clock(),
test_rng(),
)
.unwrap();
init_wallet_db_internal(
&mut db_data,
None,

View File

@ -160,6 +160,7 @@ impl RusqliteMigration for Migration {
#[cfg(test)]
mod tests {
use rand_chacha::ChaChaRng;
use rusqlite::{self, params};
use tempfile::NamedTempFile;
@ -168,7 +169,7 @@ mod tests {
use zip32::AccountId;
use crate::{
testing::db::test_clock,
testing::db::{test_clock, test_rng},
util::testing::FixedClock,
wallet::init::{init_wallet_db_internal, migrations::v_transactions_net},
WalletDb,
@ -177,8 +178,13 @@ mod tests {
#[test]
fn v_transactions_note_uniqueness_migration() {
let data_file = NamedTempFile::new().unwrap();
let mut db_data =
WalletDb::for_path(data_file.path(), Network::TestNetwork, test_clock()).unwrap();
let mut db_data = WalletDb::for_path(
data_file.path(),
Network::TestNetwork,
test_clock(),
test_rng(),
)
.unwrap();
init_wallet_db_internal(
&mut db_data,
None,
@ -213,6 +219,7 @@ mod tests {
rusqlite::Connection,
Network,
FixedClock,
ChaChaRng,
>,
expected_notes: i64| {
let mut q = db_data

View File

@ -1,8 +1,13 @@
//! Functions for transparent input support in the wallet.
use std::collections::{HashMap, HashSet};
use std::num::TryFromIntError;
use std::ops::DerefMut;
use std::rc::Rc;
use std::time::{Duration, SystemTime, SystemTimeError};
use nonempty::NonEmpty;
use rand::RngCore;
use rand_distr::Distribution;
use rusqlite::types::Value;
use rusqlite::OptionalExtension;
use rusqlite::{named_params, Connection, Row};
@ -14,7 +19,10 @@ use ::transparent::{
};
use zcash_address::unified::{Ivk, Typecode, Uivk};
use zcash_client_backend::{
data_api::{Account, AccountBalance, TransactionDataRequest},
data_api::{
Account, AccountBalance, OutputStatusFilter, TransactionDataRequest,
TransactionStatusFilter,
},
wallet::{TransparentAddressMetadata, WalletTransparentOutput},
};
use zcash_keys::{
@ -30,7 +38,7 @@ use zcash_protocol::{
};
use zip32::Scope;
use super::encoding::ReceiverFlags;
use super::encoding::{decode_epoch_seconds, ReceiverFlags};
use super::{
account_birthday_internal, chain_tip_height,
encoding::{decode_diversifier_index_be, encode_diversifier_index_be},
@ -1014,6 +1022,73 @@ pub(crate) fn put_received_transparent_utxo<P: consensus::Parameters>(
)
}
/// An enumeration of the types of errors that can occur when scheduling an event to happen at a
/// specific time.
#[derive(Debug, Clone)]
pub enum SchedulingError {
/// An error occurred in sampling a time offset using an exponential distribution.
Distribution(rand_distr::ExpError),
/// The system attempted to generate an invalid timestamp.
Time(SystemTimeError),
/// A generated duration was out of the range of valid integer values for durations.
OutOfRange(TryFromIntError),
}
impl std::fmt::Display for SchedulingError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self {
SchedulingError::Distribution(e) => {
write!(f, "Failure in sampling scheduling time: {}", e)
}
SchedulingError::Time(t) => write!(f, "Invalid system time: {}", t),
SchedulingError::OutOfRange(t) => write!(f, "Not a valid timestamp or duration: {}", t),
}
}
}
impl std::error::Error for SchedulingError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self {
SchedulingError::Distribution(_) => None,
SchedulingError::Time(t) => Some(t),
SchedulingError::OutOfRange(i) => Some(i),
}
}
}
impl From<rand_distr::ExpError> for SchedulingError {
fn from(value: rand_distr::ExpError) -> Self {
SchedulingError::Distribution(value)
}
}
impl From<SystemTimeError> for SchedulingError {
fn from(value: SystemTimeError) -> Self {
SchedulingError::Time(value)
}
}
impl From<TryFromIntError> for SchedulingError {
fn from(value: TryFromIntError) -> Self {
SchedulingError::OutOfRange(value)
}
}
/// Sample a random timestamp from an exponential distribution such that the expected value of the
/// generated timestamp is `check_interval_seconds` after the provided `from_event` time.
pub(crate) fn next_check_time<R: RngCore, D: DerefMut<Target = R>>(
mut rng: D,
from_event: SystemTime,
check_interval_seconds: u32,
) -> Result<SystemTime, SchedulingError> {
// A λ parameter of 1/check_interval_seconds will result in a distribution with an expected
// value of `check_interval_seconds`.
let dist = rand_distr::Exp::new(1.0 / f64::from(check_interval_seconds))?;
let event_delay = dist.sample(rng.deref_mut()).round() as u64;
Ok(from_event + Duration::new(event_delay, 0))
}
/// Returns the vector of [`TransactionDataRequest`]s that represents the information needed by the
/// wallet backend in order to be able to present a complete view of wallet history and memo data.
pub(crate) fn transaction_data_requests<P: consensus::Parameters>(
@ -1023,39 +1098,90 @@ pub(crate) fn transaction_data_requests<P: consensus::Parameters>(
// `lightwalletd` will return an error for `GetTaddressTxids` requests having an end height
// greater than the current chain tip height, so we take the chain tip height into account
// here in order to make this pothole easier for clients of the API to avoid.
let chain_tip_height = super::chain_tip_height(conn)?;
let chain_tip_height =
super::chain_tip_height(conn)?.ok_or(SqliteClientError::ChainHeightUnknown)?;
// We cannot construct address-based transaction data requests for the case where we cannot
// determine the height at which to begin, so we require that either the target height or mined
// height be set.
let mut address_request_stmt = conn.prepare_cached(
"SELECT ssq.address, IFNULL(t.target_height, t.mined_height)
let mut spend_requests_stmt = conn.prepare_cached(
"SELECT
ssq.address,
IFNULL(t.target_height, t.mined_height)
FROM transparent_spend_search_queue ssq
JOIN transactions t ON t.id_tx = ssq.transaction_id
WHERE t.target_height IS NOT NULL
OR t.mined_height IS NOT NULL",
)?;
let result = address_request_stmt
.query_and_then([], |row| {
let spend_search_rows = spend_requests_stmt.query_and_then([], |row| {
let address = TransparentAddress::decode(params, &row.get::<_, String>(0)?)?;
let block_range_start = BlockHeight::from(row.get::<_, u32>(1)?);
let max_end_height = block_range_start + DEFAULT_TX_EXPIRY_DELTA + 1;
Ok::<TransactionDataRequest, SqliteClientError>(
TransactionDataRequest::TransactionsInvolvingAddress {
address,
block_range_start,
block_range_end: Some(std::cmp::min(chain_tip_height + 1, max_end_height)),
request_at: None,
tx_status_filter: TransactionStatusFilter::Mined,
output_status_filter: OutputStatusFilter::All,
},
)
})?;
// Since we don't want to interpret funds that are temporarily held by an ephemeral address in
// the course of creating ZIP 320 transaction pair as belonging to the wallet, we will perform
// ephemeral address checks only for addresses that do not have an unexpired transaction
// associated with them in the database. If, for some reason, the second transaction in a ZIP
// 320 pair fails to be mined after the first transaction in the pair succeeded, we will begin
// including the associated ephemeral address in the set to be checked for funds only after
// the transaction that spends from it has expired.
let mut ephemeral_check_stmt = conn.prepare_cached(
"SELECT
cached_transparent_receiver_address,
transparent_receiver_next_check_time
FROM addresses
WHERE key_scope = :ephemeral_key_scope
AND NOT EXISTS (
SELECT 'x'
FROM transparent_received_outputs tro
JOIN transactions t ON t.id_tx = tro.transaction_id
WHERE tro.address_id = addresses.id
AND t.expiry_height > :chain_tip_height
)",
)?;
let ephemeral_check_rows = ephemeral_check_stmt.query_and_then(
named_params! {
":ephemeral_key_scope": KeyScope::Ephemeral.encode(),
":chain_tip_height": u32::from(chain_tip_height)
},
|row| {
let address = TransparentAddress::decode(params, &row.get::<_, String>(0)?)?;
let block_range_start = BlockHeight::from(row.get::<_, u32>(1)?);
let max_end_height = block_range_start + DEFAULT_TX_EXPIRY_DELTA + 1;
let request_at = row
.get::<_, Option<i64>>(1)?
.map(decode_epoch_seconds)
.transpose()?;
Ok::<TransactionDataRequest, SqliteClientError>(
TransactionDataRequest::SpendsFromAddress {
TransactionDataRequest::TransactionsInvolvingAddress {
address,
block_range_start,
block_range_end: Some(
chain_tip_height
.map_or(max_end_height, |h| std::cmp::min(h + 1, max_end_height)),
),
// We don't want these queries to leak anything about when the wallet created
// or exposed the address, so we just query for all UTXOs for the address.
block_range_start: BlockHeight::from(0),
block_range_end: None,
request_at,
tx_status_filter: TransactionStatusFilter::All,
output_status_filter: OutputStatusFilter::Unspent,
},
)
})?
.collect::<Result<Vec<_>, _>>()?;
},
)?;
Ok(result)
spend_search_rows
.chain(ephemeral_check_rows)
.collect::<Result<Vec<_>, _>>()
}
pub(crate) fn get_transparent_address_metadata<P: consensus::Parameters>(
@ -1419,7 +1545,7 @@ mod tests {
.update_chain_tip(birthday.height())
.unwrap();
let check = |db: &WalletDb<_, _, _>, account_id| {
let check = |db: &WalletDb<_, _, _, _>, account_id| {
eprintln!("checking {account_id:?}");
assert_matches!(
find_gap_start(&db.conn, account_id, KeyScope::Ephemeral, db.gap_limits.ephemeral()), Ok(addr_index)

View File

@ -1,6 +1,7 @@
//! Functions for wallet support of ephemeral transparent addresses.
use std::ops::Range;
use rand::{seq::SliceRandom, RngCore};
use rusqlite::{named_params, OptionalExtension};
use ::transparent::{
@ -11,7 +12,17 @@ use zcash_client_backend::wallet::TransparentAddressMetadata;
use zcash_keys::encoding::AddressCodec;
use zcash_protocol::consensus;
use crate::{error::SqliteClientError, wallet::KeyScope, AccountRef, AccountUuid};
use crate::{
error::SqliteClientError,
util::Clock,
wallet::{
encoding::{decode_epoch_seconds, epoch_seconds},
KeyScope,
},
AccountRef, AccountUuid,
};
use super::next_check_time;
// Returns `TransparentAddressMetadata` in the ephemeral scope for the
// given address index.
@ -85,3 +96,61 @@ pub(crate) fn find_account_for_ephemeral_address_str(
)
.optional()?)
}
pub(crate) fn schedule_ephemeral_address_checks<C: Clock, R: RngCore>(
conn: &rusqlite::Transaction,
clock: C,
mut rng: R,
) -> Result<(), SqliteClientError> {
let mut addr_check_times = conn.prepare(
"SELECT id, transparent_receiver_next_check_time
FROM addresses
WHERE key_scope = :ephemeral_key_scope
ORDER BY transparent_receiver_next_check_time NULLS FIRST",
)?;
let mut rows = addr_check_times
.query_and_then(
named_params! {
":ephemeral_key_scope": KeyScope::Ephemeral.encode()
},
|row| {
let id: i64 = row.get("id")?;
let next_check = row
.get::<_, Option<i64>>("transparent_receiver_next_check_time")?
.map(decode_epoch_seconds)
.transpose()?;
Ok::<_, SqliteClientError>((id, next_check))
},
)?
.collect::<Result<Vec<_>, _>>()?;
if let Some((_, max_check_time)) = rows.last().as_ref() {
let mut set_check_time = conn.prepare(
"UPDATE addresses
SET transparent_receiver_next_check_time = :next_check
WHERE id = :address_id",
)?;
// Set the expected value of the check time such that each ephemeral address will be
// checked once per day.
let check_interval =
(24 * 60 * 60) / u32::try_from(rows.len()).expect("number of addresses fits in a u32");
let start_time = clock.now();
let mut check_time = max_check_time.map_or(start_time, |t| std::cmp::max(t, start_time));
// Shuffle the addresses so that we don't always check them in the same order.
rows.shuffle(&mut rng);
for (address_id, addr_check_time) in rows {
// if the check time for this address is absent or in the past, schedule a check.
if addr_check_time.iter().all(|t| *t < start_time) {
check_time = next_check_time(&mut rng, check_time, check_interval)?;
set_check_time.execute(named_params! {
":next_check": epoch_seconds(check_time)?,
":address_id": address_id
})?;
}
}
}
Ok(())
}