Merge pull request #1736 from nuttycom/feature/taddr_fetch_scheduling
This commit is contained in:
commit
4130409eb0
|
@ -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",
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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,
|
||||
¶ms,
|
||||
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,
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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];
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue