Move most remaining code for wallet support of ephemeral addresses into
`zcash_client_sqlite::wallet::transparent::ephemeral`. Signed-off-by: Daira-Emma Hopwood <daira@jacaranda.org>
This commit is contained in:
parent
e164b59329
commit
914acb57ce
|
@ -540,7 +540,7 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
|
||||||
account: Self::AccountId,
|
account: Self::AccountId,
|
||||||
for_detection: bool,
|
for_detection: bool,
|
||||||
) -> Result<HashMap<TransparentAddress, Option<TransparentAddressMetadata>>, Self::Error> {
|
) -> Result<HashMap<TransparentAddress, Option<TransparentAddressMetadata>>, Self::Error> {
|
||||||
wallet::transparent::get_reserved_ephemeral_addresses(
|
wallet::transparent::ephemeral::get_reserved_ephemeral_addresses(
|
||||||
self.conn.borrow(),
|
self.conn.borrow(),
|
||||||
&self.params,
|
&self.params,
|
||||||
account,
|
account,
|
||||||
|
@ -1294,7 +1294,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
||||||
// *reliably* mined, because that is strictly more conservative in avoiding
|
// *reliably* mined, because that is strictly more conservative in avoiding
|
||||||
// going over the gap limit.
|
// going over the gap limit.
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
wallet::transparent::mark_ephemeral_address_as_mined(wdb, &address, tx_ref)?;
|
wallet::transparent::ephemeral::mark_ephemeral_address_as_mined(wdb, &address, tx_ref)?;
|
||||||
|
|
||||||
let receiver = Receiver::Transparent(address);
|
let receiver = Receiver::Transparent(address);
|
||||||
|
|
||||||
|
@ -1450,7 +1450,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
||||||
ephemeral_address,
|
ephemeral_address,
|
||||||
*receiving_account,
|
*receiving_account,
|
||||||
)?;
|
)?;
|
||||||
wallet::transparent::mark_ephemeral_address_as_used(
|
wallet::transparent::ephemeral::mark_ephemeral_address_as_used(
|
||||||
wdb,
|
wdb,
|
||||||
ephemeral_address,
|
ephemeral_address,
|
||||||
tx_ref,
|
tx_ref,
|
||||||
|
@ -1459,7 +1459,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
||||||
#[cfg(feature = "transparent-inputs")]
|
#[cfg(feature = "transparent-inputs")]
|
||||||
Recipient::External(zcash_address, PoolType::Transparent) => {
|
Recipient::External(zcash_address, PoolType::Transparent) => {
|
||||||
// Always reject sending to one of our ephemeral addresses.
|
// Always reject sending to one of our ephemeral addresses.
|
||||||
wallet::transparent::check_address_is_not_ephemeral(
|
wallet::transparent::ephemeral::check_address_is_not_ephemeral(
|
||||||
wdb,
|
wdb,
|
||||||
&zcash_address.encode(),
|
&zcash_address.encode(),
|
||||||
)?;
|
)?;
|
||||||
|
@ -1485,7 +1485,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
||||||
n: i32,
|
n: i32,
|
||||||
) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, Self::Error> {
|
) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, Self::Error> {
|
||||||
self.transactionally(|wdb| {
|
self.transactionally(|wdb| {
|
||||||
wallet::transparent::reserve_next_n_ephemeral_addresses(wdb, account_id, n)
|
wallet::transparent::ephemeral::reserve_next_n_ephemeral_addresses(wdb, account_id, n)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//! Functions for transparent input support in the wallet.
|
//! Functions for transparent input support in the wallet.
|
||||||
use std::cmp::max;
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use rusqlite::OptionalExtension;
|
use rusqlite::OptionalExtension;
|
||||||
|
@ -9,31 +8,23 @@ use zip32::{DiversifierIndex, Scope};
|
||||||
use zcash_address::unified::{Encoding, Ivk, Uivk};
|
use zcash_address::unified::{Encoding, Ivk, Uivk};
|
||||||
use zcash_client_backend::{
|
use zcash_client_backend::{
|
||||||
data_api::AccountBalance,
|
data_api::AccountBalance,
|
||||||
keys::AddressGenerationError,
|
|
||||||
wallet::{TransparentAddressMetadata, WalletTransparentOutput},
|
wallet::{TransparentAddressMetadata, WalletTransparentOutput},
|
||||||
};
|
};
|
||||||
use zcash_keys::{
|
use zcash_keys::{address::Address, encoding::AddressCodec};
|
||||||
address::Address,
|
|
||||||
encoding::{encode_transparent_address_p, AddressCodec},
|
|
||||||
};
|
|
||||||
use zcash_primitives::{
|
use zcash_primitives::{
|
||||||
legacy::{
|
legacy::{
|
||||||
keys::{EphemeralIvk, IncomingViewingKey, NonHardenedChildIndex},
|
keys::{IncomingViewingKey, NonHardenedChildIndex},
|
||||||
Script, TransparentAddress,
|
Script, TransparentAddress,
|
||||||
},
|
},
|
||||||
transaction::{
|
transaction::components::{amount::NonNegativeAmount, Amount, OutPoint, TxOut},
|
||||||
components::{amount::NonNegativeAmount, Amount, OutPoint, TxOut},
|
|
||||||
TxId,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use zcash_protocol::consensus::{self, BlockHeight};
|
use zcash_protocol::consensus::{self, BlockHeight};
|
||||||
|
|
||||||
use crate::{error::SqliteClientError, AccountId, UtxoId};
|
use crate::{error::SqliteClientError, AccountId, UtxoId};
|
||||||
use crate::{SqlTransaction, WalletDb};
|
|
||||||
|
|
||||||
use super::{chain_tip_height, get_account, get_account_ids};
|
use super::{chain_tip_height, get_account_ids};
|
||||||
|
|
||||||
mod ephemeral;
|
pub(crate) mod ephemeral;
|
||||||
|
|
||||||
pub(crate) fn detect_spending_accounts<'a>(
|
pub(crate) fn detect_spending_accounts<'a>(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
|
@ -608,248 +599,6 @@ pub(crate) fn put_transparent_output<P: consensus::Parameters>(
|
||||||
Ok(utxo_id)
|
Ok(utxo_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If `address` is one of our ephemeral addresses, mark it as having an output
|
|
||||||
/// in a transaction that we have just created. This has no effect if `address` is
|
|
||||||
/// not one of our ephemeral addresses.
|
|
||||||
///
|
|
||||||
/// Returns a `SqliteClientError::EphemeralAddressReuse` error if the address was
|
|
||||||
/// already used.
|
|
||||||
pub(crate) fn mark_ephemeral_address_as_used<P: consensus::Parameters>(
|
|
||||||
wdb: &mut WalletDb<SqlTransaction<'_>, P>,
|
|
||||||
ephemeral_address: &TransparentAddress,
|
|
||||||
tx_ref: i64,
|
|
||||||
) -> Result<(), SqliteClientError> {
|
|
||||||
let address_str = encode_transparent_address_p(&wdb.params, ephemeral_address);
|
|
||||||
ephemeral_address_check_internal(wdb, &address_str, false)?;
|
|
||||||
|
|
||||||
wdb.conn.0.execute(
|
|
||||||
"UPDATE ephemeral_addresses SET used_in_tx = :used_in_tx WHERE address = :address",
|
|
||||||
named_params![":used_in_tx": &tx_ref, ":address": address_str],
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a `SqliteClientError::EphemeralAddressReuse` error if `address` is
|
|
||||||
/// an ephemeral transparent address.
|
|
||||||
pub(crate) fn check_address_is_not_ephemeral<P: consensus::Parameters>(
|
|
||||||
wdb: &mut WalletDb<SqlTransaction<'_>, P>,
|
|
||||||
address_str: &str,
|
|
||||||
) -> Result<(), SqliteClientError> {
|
|
||||||
ephemeral_address_check_internal(wdb, address_str, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a `SqliteClientError::EphemeralAddressReuse` error if the address was
|
|
||||||
/// already used. If `reject_all_ephemeral` is set, return an error if the address
|
|
||||||
/// is ephemeral at all, regardless of reuse.
|
|
||||||
fn ephemeral_address_check_internal<P: consensus::Parameters>(
|
|
||||||
wdb: &mut WalletDb<SqlTransaction<'_>, P>,
|
|
||||||
address_str: &str,
|
|
||||||
reject_all_ephemeral: bool,
|
|
||||||
) -> Result<(), SqliteClientError> {
|
|
||||||
// It is intentional that we don't require `t.mined_height` to be non-null.
|
|
||||||
// That is, we conservatively treat an ephemeral address as potentially
|
|
||||||
// reused even if we think that the transaction where we had evidence of
|
|
||||||
// its use is at present unmined. This should never occur in supported
|
|
||||||
// situations where only a single correctly operating wallet instance is
|
|
||||||
// using a given seed, because such a wallet will not reuse an address that
|
|
||||||
// it ever reserved.
|
|
||||||
//
|
|
||||||
// `COALESCE(used_in_tx, mined_in_tx)` can only differ from `used_in_tx`
|
|
||||||
// if the address was reserved, an error occurred in transaction creation
|
|
||||||
// before calling `mark_ephemeral_address_as_used`, and then we observed
|
|
||||||
// the address to have been used in a mined transaction (presumably by
|
|
||||||
// another wallet instance, or due to a bug) anyway.
|
|
||||||
let res = wdb
|
|
||||||
.conn
|
|
||||||
.0
|
|
||||||
.query_row(
|
|
||||||
"SELECT t.txid FROM ephemeral_addresses
|
|
||||||
LEFT OUTER JOIN transactions t
|
|
||||||
ON t.id_tx = COALESCE(used_in_tx, mined_in_tx)
|
|
||||||
WHERE address = :address",
|
|
||||||
named_params![":address": address_str],
|
|
||||||
|row| row.get::<_, Option<Vec<u8>>>(0),
|
|
||||||
)
|
|
||||||
.optional()?;
|
|
||||||
|
|
||||||
match res {
|
|
||||||
Some(Some(txid_bytes)) => {
|
|
||||||
let txid = TxId::from_bytes(
|
|
||||||
txid_bytes
|
|
||||||
.try_into()
|
|
||||||
.map_err(|_| SqliteClientError::CorruptedData("invalid txid".to_owned()))?,
|
|
||||||
);
|
|
||||||
Err(SqliteClientError::EphemeralAddressReuse(
|
|
||||||
address_str.to_owned(),
|
|
||||||
Some(txid),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
Some(None) if reject_all_ephemeral => Err(SqliteClientError::EphemeralAddressReuse(
|
|
||||||
address_str.to_owned(),
|
|
||||||
None,
|
|
||||||
)),
|
|
||||||
_ => Ok(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If `address` is one of our ephemeral addresses, mark it as having an output
|
|
||||||
/// in the given mined transaction (which may or may not be a transaction we sent).
|
|
||||||
///
|
|
||||||
/// `tx_ref` must be a valid transaction reference. This call has no effect if
|
|
||||||
/// `address` is not one of our ephemeral addresses.
|
|
||||||
pub(crate) fn mark_ephemeral_address_as_mined<P: consensus::Parameters>(
|
|
||||||
wdb: &mut WalletDb<SqlTransaction<'_>, P>,
|
|
||||||
address: &TransparentAddress,
|
|
||||||
tx_ref: i64,
|
|
||||||
) -> Result<(), SqliteClientError> {
|
|
||||||
let address_str = encode_transparent_address_p(&wdb.params, address);
|
|
||||||
|
|
||||||
// Figure out which transaction was mined earlier: `tx_ref`, or any existing
|
|
||||||
// tx referenced by `mined_in_tx` for the given address. Prefer the existing
|
|
||||||
// reference in case of a tie or if both transactions are unmined.
|
|
||||||
// This slightly reduces the chance of unnecessarily reaching the gap limit
|
|
||||||
// too early in some corner cases (because the earlier transaction is less
|
|
||||||
// likely to be unmined).
|
|
||||||
//
|
|
||||||
// The query should always return a value if `tx_ref` is valid.
|
|
||||||
let earlier_ref = wdb.conn.0.query_row(
|
|
||||||
"SELECT id_tx FROM transactions
|
|
||||||
LEFT OUTER JOIN ephemeral_addresses e
|
|
||||||
ON id_tx = e.mined_in_tx
|
|
||||||
WHERE id_tx = :tx_ref OR e.address = :address
|
|
||||||
ORDER BY mined_height ASC NULLS LAST,
|
|
||||||
tx_index ASC NULLS LAST,
|
|
||||||
e.mined_in_tx ASC NULLS LAST
|
|
||||||
LIMIT 1",
|
|
||||||
named_params![":tx_ref": &tx_ref, ":address": address_str],
|
|
||||||
|row| row.get::<_, i64>(0),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
wdb.conn.0.execute(
|
|
||||||
"UPDATE ephemeral_addresses SET mined_in_tx = :mined_in_tx WHERE address = :address",
|
|
||||||
named_params![":mined_in_tx": &earlier_ref, ":address": address_str],
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the ephemeral transparent IVK for a given account ID.
|
|
||||||
pub(crate) fn get_ephemeral_ivk<P: consensus::Parameters>(
|
|
||||||
conn: &rusqlite::Connection,
|
|
||||||
params: &P,
|
|
||||||
account_id: AccountId,
|
|
||||||
) -> Result<EphemeralIvk, SqliteClientError> {
|
|
||||||
use zcash_client_backend::data_api::Account;
|
|
||||||
|
|
||||||
Ok(get_account(conn, params, account_id)?
|
|
||||||
.ok_or(SqliteClientError::AccountUnknown)?
|
|
||||||
.ufvk()
|
|
||||||
.and_then(|ufvk| ufvk.transparent())
|
|
||||||
.ok_or(SqliteClientError::UnknownZip32Derivation)?
|
|
||||||
.derive_ephemeral_ivk()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a vector with all ephemeral transparent addresses potentially belonging to this wallet.
|
|
||||||
/// If `for_detection` is true, this includes addresses for an additional GAP_LIMIT indices.
|
|
||||||
pub(crate) fn get_reserved_ephemeral_addresses<P: consensus::Parameters>(
|
|
||||||
conn: &rusqlite::Connection,
|
|
||||||
params: &P,
|
|
||||||
account_id: AccountId,
|
|
||||||
for_detection: bool,
|
|
||||||
) -> Result<HashMap<TransparentAddress, Option<TransparentAddressMetadata>>, SqliteClientError> {
|
|
||||||
let mut stmt = conn.prepare(
|
|
||||||
"SELECT address, address_index FROM ephemeral_addresses WHERE account_id = :account ORDER BY address_index",
|
|
||||||
)?;
|
|
||||||
let mut rows = stmt.query(named_params! { ":account": account_id.0 })?;
|
|
||||||
|
|
||||||
let mut result = HashMap::new();
|
|
||||||
let mut first_unused_index: Option<i32> = Some(0);
|
|
||||||
|
|
||||||
while let Some(row) = rows.next()? {
|
|
||||||
let addr_str: String = row.get(0)?;
|
|
||||||
let raw_index: u32 = row.get(1)?;
|
|
||||||
first_unused_index = i32::try_from(raw_index)
|
|
||||||
.map_err(|e| SqliteClientError::CorruptedData(e.to_string()))?
|
|
||||||
.checked_add(1);
|
|
||||||
let address_index = NonHardenedChildIndex::from_index(raw_index).unwrap();
|
|
||||||
let address = TransparentAddress::decode(params, &addr_str)?;
|
|
||||||
result.insert(address, Some(ephemeral::metadata(address_index)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if for_detection {
|
|
||||||
if let Some(first) = first_unused_index {
|
|
||||||
let ephemeral_ivk = get_ephemeral_ivk(conn, params, account_id)?;
|
|
||||||
|
|
||||||
for raw_index in ephemeral::range_after(first, ephemeral::GAP_LIMIT) {
|
|
||||||
let address_index = NonHardenedChildIndex::from_index(raw_index).unwrap();
|
|
||||||
let address = ephemeral_ivk.derive_ephemeral_address(address_index)?;
|
|
||||||
result.insert(address, Some(ephemeral::metadata(address_index)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a vector with the next `n` previously unreserved ephemeral addresses for
|
|
||||||
/// the given account.
|
|
||||||
///
|
|
||||||
/// Precondition: `n >= 0`
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// * `SqliteClientError::AccountUnknown`, if there is no account with the given id.
|
|
||||||
/// * `SqliteClientError::UnknownZip32Derivation`, if the account is imported and
|
|
||||||
/// it is not possible to derive new addresses for it.
|
|
||||||
/// * `SqliteClientError::ReachedGapLimit`, if it is not possible to reserve `n` addresses
|
|
||||||
/// within the gap limit after the last address in this account that is known to have an
|
|
||||||
/// output in a mined transaction.
|
|
||||||
/// * `SqliteClientError::AddressGeneration(AddressGenerationError::DiversifierSpaceExhausted)`,
|
|
||||||
/// if the limit on transparent address indices has been reached.
|
|
||||||
pub(crate) fn reserve_next_n_ephemeral_addresses<P: consensus::Parameters>(
|
|
||||||
wdb: &mut WalletDb<SqlTransaction<'_>, P>,
|
|
||||||
account_id: AccountId,
|
|
||||||
n: i32,
|
|
||||||
) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, SqliteClientError> {
|
|
||||||
if n == 0 {
|
|
||||||
return Ok(vec![]);
|
|
||||||
}
|
|
||||||
assert!(n > 0);
|
|
||||||
|
|
||||||
let ephemeral_ivk = get_ephemeral_ivk(wdb.conn.0, &wdb.params, account_id)?;
|
|
||||||
let last_reserved_index = ephemeral::last_reserved_index(wdb.conn.0, account_id)?;
|
|
||||||
let last_safe_index = ephemeral::last_safe_index(wdb.conn.0, account_id)?;
|
|
||||||
let allocation = ephemeral::range_after(last_reserved_index, n);
|
|
||||||
|
|
||||||
if allocation.clone().count() < n.try_into().unwrap() {
|
|
||||||
return Err(SqliteClientError::AddressGeneration(
|
|
||||||
AddressGenerationError::DiversifierSpaceExhausted,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if *allocation.end() > last_safe_index {
|
|
||||||
let unsafe_index = max(*allocation.start(), last_safe_index.saturating_add(1));
|
|
||||||
return Err(SqliteClientError::ReachedGapLimit(account_id, unsafe_index));
|
|
||||||
}
|
|
||||||
|
|
||||||
// used_in_tx and mined_in_tx are initially NULL
|
|
||||||
let mut stmt_insert_ephemeral_address = wdb.conn.0.prepare_cached(
|
|
||||||
"INSERT INTO ephemeral_addresses (account_id, address_index, address)
|
|
||||||
VALUES (:account_id, :address_index, :address)",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
allocation
|
|
||||||
.map(|raw_index| {
|
|
||||||
let address_index = NonHardenedChildIndex::from_index(raw_index).unwrap();
|
|
||||||
let address = ephemeral_ivk.derive_ephemeral_address(address_index)?;
|
|
||||||
|
|
||||||
stmt_insert_ephemeral_address.execute(named_params![
|
|
||||||
":account_id": account_id.0,
|
|
||||||
":address_index": raw_index,
|
|
||||||
":address": encode_transparent_address_p(&wdb.params, &address)
|
|
||||||
])?;
|
|
||||||
Ok((address, ephemeral::metadata(address_index)))
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::testing::{AddressType, TestBuilder, TestState};
|
use crate::testing::{AddressType, TestBuilder, TestState};
|
||||||
|
|
|
@ -1,12 +1,25 @@
|
||||||
//! Functions for wallet support of ephemeral transparent addresses.
|
//! Functions for wallet support of ephemeral transparent addresses.
|
||||||
|
use std::cmp::max;
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::ops::RangeInclusive;
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
use rusqlite::{named_params, OptionalExtension};
|
use rusqlite::{named_params, OptionalExtension};
|
||||||
|
|
||||||
use zcash_client_backend::wallet::TransparentAddressMetadata;
|
use zcash_client_backend::{data_api::Account, wallet::TransparentAddressMetadata};
|
||||||
use zcash_primitives::legacy::keys::{NonHardenedChildIndex, TransparentKeyScope};
|
use zcash_keys::{
|
||||||
|
encoding::{encode_transparent_address_p, AddressCodec},
|
||||||
|
keys::AddressGenerationError,
|
||||||
|
};
|
||||||
|
use zcash_primitives::{
|
||||||
|
legacy::{
|
||||||
|
keys::{EphemeralIvk, NonHardenedChildIndex, TransparentKeyScope},
|
||||||
|
TransparentAddress,
|
||||||
|
},
|
||||||
|
transaction::TxId,
|
||||||
|
};
|
||||||
|
use zcash_protocol::consensus;
|
||||||
|
|
||||||
use crate::{error::SqliteClientError, AccountId};
|
use crate::{error::SqliteClientError, wallet::get_account, AccountId, SqlTransaction, WalletDb};
|
||||||
|
|
||||||
/// The number of ephemeral addresses that can be safely reserved without observing any
|
/// The number of ephemeral addresses that can be safely reserved without observing any
|
||||||
/// of them to be mined. This is the same as the gap limit in Bitcoin.
|
/// of them to be mined. This is the same as the gap limit in Bitcoin.
|
||||||
|
@ -16,14 +29,14 @@ pub(crate) const GAP_LIMIT: i32 = 20;
|
||||||
// TODO: consider moving this to `zcash_primitives::legacy::keys`, or else
|
// TODO: consider moving this to `zcash_primitives::legacy::keys`, or else
|
||||||
// provide a way to derive `ivk`s for custom scopes in general there, so that
|
// provide a way to derive `ivk`s for custom scopes in general there, so that
|
||||||
// the constant isn't duplicated.
|
// the constant isn't duplicated.
|
||||||
pub(crate) const EPHEMERAL_SCOPE: TransparentKeyScope = match TransparentKeyScope::custom(2) {
|
const EPHEMERAL_SCOPE: TransparentKeyScope = match TransparentKeyScope::custom(2) {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => unreachable!(),
|
None => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Returns `TransparentAddressMetadata` in the ephemeral scope for the
|
// Returns `TransparentAddressMetadata` in the ephemeral scope for the
|
||||||
// given address index.
|
// given address index.
|
||||||
pub(crate) fn metadata(address_index: NonHardenedChildIndex) -> TransparentAddressMetadata {
|
fn metadata(address_index: NonHardenedChildIndex) -> TransparentAddressMetadata {
|
||||||
TransparentAddressMetadata::new(EPHEMERAL_SCOPE, address_index)
|
TransparentAddressMetadata::new(EPHEMERAL_SCOPE, address_index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,3 +109,243 @@ pub(crate) fn range_after(i: i32, n: i32) -> RangeInclusive<u32> {
|
||||||
let last = u32::try_from(i.saturating_add(n)).unwrap();
|
let last = u32::try_from(i.saturating_add(n)).unwrap();
|
||||||
first..=last
|
first..=last
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the ephemeral transparent IVK for a given account ID.
|
||||||
|
pub(crate) fn get_ephemeral_ivk<P: consensus::Parameters>(
|
||||||
|
conn: &rusqlite::Connection,
|
||||||
|
params: &P,
|
||||||
|
account_id: AccountId,
|
||||||
|
) -> Result<EphemeralIvk, SqliteClientError> {
|
||||||
|
Ok(get_account(conn, params, account_id)?
|
||||||
|
.ok_or(SqliteClientError::AccountUnknown)?
|
||||||
|
.ufvk()
|
||||||
|
.and_then(|ufvk| ufvk.transparent())
|
||||||
|
.ok_or(SqliteClientError::UnknownZip32Derivation)?
|
||||||
|
.derive_ephemeral_ivk()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a vector with all ephemeral transparent addresses potentially belonging to this wallet.
|
||||||
|
/// If `for_detection` is true, this includes addresses for an additional GAP_LIMIT indices.
|
||||||
|
pub(crate) fn get_reserved_ephemeral_addresses<P: consensus::Parameters>(
|
||||||
|
conn: &rusqlite::Connection,
|
||||||
|
params: &P,
|
||||||
|
account_id: AccountId,
|
||||||
|
for_detection: bool,
|
||||||
|
) -> Result<HashMap<TransparentAddress, Option<TransparentAddressMetadata>>, SqliteClientError> {
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT address, address_index FROM ephemeral_addresses WHERE account_id = :account ORDER BY address_index",
|
||||||
|
)?;
|
||||||
|
let mut rows = stmt.query(named_params! { ":account": account_id.0 })?;
|
||||||
|
|
||||||
|
let mut result = HashMap::new();
|
||||||
|
let mut first_unused_index: Option<i32> = Some(0);
|
||||||
|
|
||||||
|
while let Some(row) = rows.next()? {
|
||||||
|
let addr_str: String = row.get(0)?;
|
||||||
|
let raw_index: u32 = row.get(1)?;
|
||||||
|
first_unused_index = i32::try_from(raw_index)
|
||||||
|
.map_err(|e| SqliteClientError::CorruptedData(e.to_string()))?
|
||||||
|
.checked_add(1);
|
||||||
|
let address_index = NonHardenedChildIndex::from_index(raw_index).unwrap();
|
||||||
|
let address = TransparentAddress::decode(params, &addr_str)?;
|
||||||
|
result.insert(address, Some(metadata(address_index)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if for_detection {
|
||||||
|
if let Some(first) = first_unused_index {
|
||||||
|
let ephemeral_ivk = get_ephemeral_ivk(conn, params, account_id)?;
|
||||||
|
|
||||||
|
for raw_index in range_after(first, GAP_LIMIT) {
|
||||||
|
let address_index = NonHardenedChildIndex::from_index(raw_index).unwrap();
|
||||||
|
let address = ephemeral_ivk.derive_ephemeral_address(address_index)?;
|
||||||
|
result.insert(address, Some(metadata(address_index)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a vector with the next `n` previously unreserved ephemeral addresses for
|
||||||
|
/// the given account.
|
||||||
|
///
|
||||||
|
/// Precondition: `n >= 0`
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// * `SqliteClientError::AccountUnknown`, if there is no account with the given id.
|
||||||
|
/// * `SqliteClientError::UnknownZip32Derivation`, if the account is imported and
|
||||||
|
/// it is not possible to derive new addresses for it.
|
||||||
|
/// * `SqliteClientError::ReachedGapLimit`, if it is not possible to reserve `n` addresses
|
||||||
|
/// within the gap limit after the last address in this account that is known to have an
|
||||||
|
/// output in a mined transaction.
|
||||||
|
/// * `SqliteClientError::AddressGeneration(AddressGenerationError::DiversifierSpaceExhausted)`,
|
||||||
|
/// if the limit on transparent address indices has been reached.
|
||||||
|
pub(crate) fn reserve_next_n_ephemeral_addresses<P: consensus::Parameters>(
|
||||||
|
wdb: &mut WalletDb<SqlTransaction<'_>, P>,
|
||||||
|
account_id: AccountId,
|
||||||
|
n: i32,
|
||||||
|
) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, SqliteClientError> {
|
||||||
|
if n == 0 {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
assert!(n > 0);
|
||||||
|
|
||||||
|
let ephemeral_ivk = get_ephemeral_ivk(wdb.conn.0, &wdb.params, account_id)?;
|
||||||
|
let last_reserved_index = last_reserved_index(wdb.conn.0, account_id)?;
|
||||||
|
let last_safe_index = last_safe_index(wdb.conn.0, account_id)?;
|
||||||
|
let allocation = range_after(last_reserved_index, n);
|
||||||
|
|
||||||
|
if allocation.clone().count() < n.try_into().unwrap() {
|
||||||
|
return Err(SqliteClientError::AddressGeneration(
|
||||||
|
AddressGenerationError::DiversifierSpaceExhausted,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if *allocation.end() > last_safe_index {
|
||||||
|
let unsafe_index = max(*allocation.start(), last_safe_index.saturating_add(1));
|
||||||
|
return Err(SqliteClientError::ReachedGapLimit(account_id, unsafe_index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// used_in_tx and mined_in_tx are initially NULL
|
||||||
|
let mut stmt_insert_ephemeral_address = wdb.conn.0.prepare_cached(
|
||||||
|
"INSERT INTO ephemeral_addresses (account_id, address_index, address)
|
||||||
|
VALUES (:account_id, :address_index, :address)",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
allocation
|
||||||
|
.map(|raw_index| {
|
||||||
|
let address_index = NonHardenedChildIndex::from_index(raw_index).unwrap();
|
||||||
|
let address = ephemeral_ivk.derive_ephemeral_address(address_index)?;
|
||||||
|
|
||||||
|
stmt_insert_ephemeral_address.execute(named_params![
|
||||||
|
":account_id": account_id.0,
|
||||||
|
":address_index": raw_index,
|
||||||
|
":address": encode_transparent_address_p(&wdb.params, &address)
|
||||||
|
])?;
|
||||||
|
Ok((address, metadata(address_index)))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a `SqliteClientError::EphemeralAddressReuse` error if `address` is
|
||||||
|
/// an ephemeral transparent address.
|
||||||
|
pub(crate) fn check_address_is_not_ephemeral<P: consensus::Parameters>(
|
||||||
|
wdb: &mut WalletDb<SqlTransaction<'_>, P>,
|
||||||
|
address_str: &str,
|
||||||
|
) -> Result<(), SqliteClientError> {
|
||||||
|
ephemeral_address_check_internal(wdb, address_str, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a `SqliteClientError::EphemeralAddressReuse` error if the address was
|
||||||
|
/// already used. If `reject_all_ephemeral` is set, return an error if the address
|
||||||
|
/// is ephemeral at all, regardless of reuse.
|
||||||
|
fn ephemeral_address_check_internal<P: consensus::Parameters>(
|
||||||
|
wdb: &mut WalletDb<SqlTransaction<'_>, P>,
|
||||||
|
address_str: &str,
|
||||||
|
reject_all_ephemeral: bool,
|
||||||
|
) -> Result<(), SqliteClientError> {
|
||||||
|
// It is intentional that we don't require `t.mined_height` to be non-null.
|
||||||
|
// That is, we conservatively treat an ephemeral address as potentially
|
||||||
|
// reused even if we think that the transaction where we had evidence of
|
||||||
|
// its use is at present unmined. This should never occur in supported
|
||||||
|
// situations where only a single correctly operating wallet instance is
|
||||||
|
// using a given seed, because such a wallet will not reuse an address that
|
||||||
|
// it ever reserved.
|
||||||
|
//
|
||||||
|
// `COALESCE(used_in_tx, mined_in_tx)` can only differ from `used_in_tx`
|
||||||
|
// if the address was reserved, an error occurred in transaction creation
|
||||||
|
// before calling `mark_ephemeral_address_as_used`, and then we observed
|
||||||
|
// the address to have been used in a mined transaction (presumably by
|
||||||
|
// another wallet instance, or due to a bug) anyway.
|
||||||
|
let res = wdb
|
||||||
|
.conn
|
||||||
|
.0
|
||||||
|
.query_row(
|
||||||
|
"SELECT t.txid FROM ephemeral_addresses
|
||||||
|
LEFT OUTER JOIN transactions t
|
||||||
|
ON t.id_tx = COALESCE(used_in_tx, mined_in_tx)
|
||||||
|
WHERE address = :address",
|
||||||
|
named_params![":address": address_str],
|
||||||
|
|row| row.get::<_, Option<Vec<u8>>>(0),
|
||||||
|
)
|
||||||
|
.optional()?;
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Some(Some(txid_bytes)) => {
|
||||||
|
let txid = TxId::from_bytes(
|
||||||
|
txid_bytes
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| SqliteClientError::CorruptedData("invalid txid".to_owned()))?,
|
||||||
|
);
|
||||||
|
Err(SqliteClientError::EphemeralAddressReuse(
|
||||||
|
address_str.to_owned(),
|
||||||
|
Some(txid),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Some(None) if reject_all_ephemeral => Err(SqliteClientError::EphemeralAddressReuse(
|
||||||
|
address_str.to_owned(),
|
||||||
|
None,
|
||||||
|
)),
|
||||||
|
_ => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `address` is one of our ephemeral addresses, mark it as having an output
|
||||||
|
/// in a transaction that we have just created. This has no effect if `address` is
|
||||||
|
/// not one of our ephemeral addresses.
|
||||||
|
///
|
||||||
|
/// Returns a `SqliteClientError::EphemeralAddressReuse` error if the address was
|
||||||
|
/// already used.
|
||||||
|
pub(crate) fn mark_ephemeral_address_as_used<P: consensus::Parameters>(
|
||||||
|
wdb: &mut WalletDb<SqlTransaction<'_>, P>,
|
||||||
|
ephemeral_address: &TransparentAddress,
|
||||||
|
tx_ref: i64,
|
||||||
|
) -> Result<(), SqliteClientError> {
|
||||||
|
let address_str = encode_transparent_address_p(&wdb.params, ephemeral_address);
|
||||||
|
ephemeral_address_check_internal(wdb, &address_str, false)?;
|
||||||
|
|
||||||
|
wdb.conn.0.execute(
|
||||||
|
"UPDATE ephemeral_addresses SET used_in_tx = :used_in_tx WHERE address = :address",
|
||||||
|
named_params![":used_in_tx": &tx_ref, ":address": address_str],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If `address` is one of our ephemeral addresses, mark it as having an output
|
||||||
|
/// in the given mined transaction (which may or may not be a transaction we sent).
|
||||||
|
///
|
||||||
|
/// `tx_ref` must be a valid transaction reference. This call has no effect if
|
||||||
|
/// `address` is not one of our ephemeral addresses.
|
||||||
|
pub(crate) fn mark_ephemeral_address_as_mined<P: consensus::Parameters>(
|
||||||
|
wdb: &mut WalletDb<SqlTransaction<'_>, P>,
|
||||||
|
address: &TransparentAddress,
|
||||||
|
tx_ref: i64,
|
||||||
|
) -> Result<(), SqliteClientError> {
|
||||||
|
let address_str = encode_transparent_address_p(&wdb.params, address);
|
||||||
|
|
||||||
|
// Figure out which transaction was mined earlier: `tx_ref`, or any existing
|
||||||
|
// tx referenced by `mined_in_tx` for the given address. Prefer the existing
|
||||||
|
// reference in case of a tie or if both transactions are unmined.
|
||||||
|
// This slightly reduces the chance of unnecessarily reaching the gap limit
|
||||||
|
// too early in some corner cases (because the earlier transaction is less
|
||||||
|
// likely to be unmined).
|
||||||
|
//
|
||||||
|
// The query should always return a value if `tx_ref` is valid.
|
||||||
|
let earlier_ref = wdb.conn.0.query_row(
|
||||||
|
"SELECT id_tx FROM transactions
|
||||||
|
LEFT OUTER JOIN ephemeral_addresses e
|
||||||
|
ON id_tx = e.mined_in_tx
|
||||||
|
WHERE id_tx = :tx_ref OR e.address = :address
|
||||||
|
ORDER BY mined_height ASC NULLS LAST,
|
||||||
|
tx_index ASC NULLS LAST,
|
||||||
|
e.mined_in_tx ASC NULLS LAST
|
||||||
|
LIMIT 1",
|
||||||
|
named_params![":tx_ref": &tx_ref, ":address": address_str],
|
||||||
|
|row| row.get::<_, i64>(0),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
wdb.conn.0.execute(
|
||||||
|
"UPDATE ephemeral_addresses SET mined_in_tx = :mined_in_tx WHERE address = :address",
|
||||||
|
named_params![":mined_in_tx": &earlier_ref, ":address": address_str],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue