Skip to content

Commit

Permalink
zcash_client_sqlite: Move ephemeral address management test out of mi…
Browse files Browse the repository at this point in the history
…gration.

This test is not specific to the migration; it's a more general test of
ephemeral address rotation behavior and should evolve with the evolution
of address rotation and gap limit handling, not be tied to the behavior
of methods at the time that this migration was created.
  • Loading branch information
nuttycom committed Dec 30, 2024
1 parent 29d8f10 commit f48f72b
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 209 deletions.
208 changes: 0 additions & 208 deletions zcash_client_sqlite/src/wallet/init/migrations/ephemeral_addresses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,216 +85,8 @@ impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
mod tests {
use crate::wallet::init::migrations::tests::test_migrate;

#[cfg(feature = "transparent-inputs")]
use {
crate::{
error::SqliteClientError,
wallet::{
self, account_kind_code, init::init_wallet_db_internal, transparent::ephemeral,
},
AccountRef, WalletDb,
},
::transparent::keys::NonHardenedChildIndex,
rusqlite::{named_params, Connection},
secrecy::{ExposeSecret, Secret, SecretVec},
tempfile::NamedTempFile,
zcash_client_backend::data_api::GAP_LIMIT,
zcash_client_backend::{
data_api::{AccountBirthday, AccountSource},
wallet::TransparentAddressMetadata,
},
zcash_keys::keys::UnifiedSpendingKey,
zcash_primitives::block::BlockHash,
zcash_protocol::consensus::Network,
zip32::{fingerprint::SeedFingerprint, AccountId as Zip32AccountId},
};

/// This is a minimized copy of [`wallet::create_account`] as of the time of the
/// creation of this migration.
#[cfg(feature = "transparent-inputs")]
fn create_account(
wdb: &mut WalletDb<Connection, Network>,
seed: &SecretVec<u8>,
birthday: &AccountBirthday,
) -> Result<(AccountRef, UnifiedSpendingKey), SqliteClientError> {
wdb.transactionally(|wdb| {
let seed_fingerprint =
SeedFingerprint::from_seed(seed.expose_secret()).ok_or_else(|| {
SqliteClientError::BadAccountData(
"Seed must be between 32 and 252 bytes in length.".to_owned(),
)
})?;
let account_index = wallet::max_zip32_account_index(wdb.conn.0, &seed_fingerprint)?
.map(|a| {
a.next()
.ok_or(SqliteClientError::Zip32AccountIndexOutOfRange)
})
.transpose()?
.unwrap_or(zip32::AccountId::ZERO);

let usk =
UnifiedSpendingKey::from_seed(&wdb.params, seed.expose_secret(), account_index)
.map_err(|_| SqliteClientError::KeyDerivationError(account_index))?;
let ufvk = usk.to_unified_full_viewing_key();

#[cfg(feature = "orchard")]
let orchard_item = ufvk.orchard().map(|k| k.to_bytes());
#[cfg(not(feature = "orchard"))]
let orchard_item: Option<Vec<u8>> = None;

let sapling_item = ufvk.sapling().map(|k| k.to_bytes());

#[cfg(feature = "transparent-inputs")]
let transparent_item = ufvk.transparent().map(|k| k.serialize());
#[cfg(not(feature = "transparent-inputs"))]
let transparent_item: Option<Vec<u8>> = None;

let birthday_sapling_tree_size = Some(birthday.sapling_frontier().tree_size());
#[cfg(feature = "orchard")]
let birthday_orchard_tree_size = Some(birthday.orchard_frontier().tree_size());
#[cfg(not(feature = "orchard"))]
let birthday_orchard_tree_size: Option<u64> = None;

let account_id: AccountRef = wdb.conn.0.query_row(
r#"
INSERT INTO accounts (
account_kind, hd_seed_fingerprint, hd_account_index,
ufvk, uivk,
orchard_fvk_item_cache, sapling_fvk_item_cache, p2pkh_fvk_item_cache,
birthday_height, birthday_sapling_tree_size, birthday_orchard_tree_size,
recover_until_height
)
VALUES (
:account_kind, :hd_seed_fingerprint, :hd_account_index,
:ufvk, :uivk,
:orchard_fvk_item_cache, :sapling_fvk_item_cache, :p2pkh_fvk_item_cache,
:birthday_height, :birthday_sapling_tree_size, :birthday_orchard_tree_size,
:recover_until_height
)
RETURNING id;
"#,
named_params![
":account_kind": 0, // 0 == Derived
":hd_seed_fingerprint": seed_fingerprint.to_bytes(),
":hd_account_index": u32::from(account_index),
":ufvk": ufvk.encode(&wdb.params),
":uivk": ufvk.to_unified_incoming_viewing_key().encode(&wdb.params),
":orchard_fvk_item_cache": orchard_item,
":sapling_fvk_item_cache": sapling_item,
":p2pkh_fvk_item_cache": transparent_item,
":birthday_height": u32::from(birthday.height()),
":birthday_sapling_tree_size": birthday_sapling_tree_size,
":birthday_orchard_tree_size": birthday_orchard_tree_size,
":recover_until_height": birthday.recover_until().map(u32::from)
],
|row| Ok(AccountRef(row.get(0)?)),
)?;

// Initialize the `ephemeral_addresses` table.
#[cfg(feature = "transparent-inputs")]
wallet::transparent::ephemeral::init_account(wdb.conn.0, &wdb.params, account_id)?;

Ok((account_id, usk))
})
}

#[test]
fn migrate() {
test_migrate(&[super::MIGRATION_ID]);
}

#[test]
#[cfg(feature = "transparent-inputs")]
fn initialize_table() {
use zcash_client_backend::data_api::Zip32Derivation;

let network = Network::TestNetwork;
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), network).unwrap();

let seed0 = vec![0x00; 32];
init_wallet_db_internal(
&mut db_data,
Some(Secret::new(seed0.clone())),
super::DEPENDENCIES,
false,
)
.unwrap();

let birthday = AccountBirthday::from_sapling_activation(&network, BlockHash([0; 32]));

// Simulate creating an account prior to this migration.
let account0_index = Zip32AccountId::ZERO;
let account0_seed_fp = [0u8; 32];
let account0_kind = account_kind_code(&AccountSource::Derived {
derivation: Zip32Derivation::new(
SeedFingerprint::from_seed(&account0_seed_fp).unwrap(),
account0_index,
),
key_source: None,
});
assert_eq!(u32::from(account0_index), 0);
let account0_id = AccountRef(0);

let usk0 = UnifiedSpendingKey::from_seed(&network, &seed0, account0_index).unwrap();
let ufvk0 = usk0.to_unified_full_viewing_key();
let uivk0 = ufvk0.to_unified_incoming_viewing_key();

db_data
.conn
.execute(
"INSERT INTO accounts (id, account_kind, hd_seed_fingerprint, hd_account_index, ufvk, uivk, birthday_height)
VALUES (:id, :account_kind, :hd_seed_fingerprint, :hd_account_index, :ufvk, :uivk, :birthday_height)",
named_params![
":id": account0_id.0,
":account_kind": account0_kind,
":hd_seed_fingerprint": account0_seed_fp,
":hd_account_index": u32::from(account0_index),
":ufvk": ufvk0.encode(&network),
":uivk": uivk0.encode(&network),
":birthday_height": u32::from(birthday.height()),
],
)
.unwrap();

// The `ephemeral_addresses` table is expected not to exist before migration.
assert_matches!(
ephemeral::first_unstored_index(&db_data.conn, account0_id),
Err(SqliteClientError::DbError(_))
);

let check = |db: &WalletDb<_, _>, account_id| {
eprintln!("checking {account_id:?}");
assert_matches!(ephemeral::first_unstored_index(&db.conn, account_id), Ok(addr_index) if addr_index == GAP_LIMIT);
assert_matches!(ephemeral::first_unreserved_index(&db.conn, account_id), Ok(addr_index) if addr_index == 0);

let known_addrs =
ephemeral::get_known_ephemeral_addresses(&db.conn, &db.params, account_id, None)
.unwrap();

let expected_metadata: Vec<TransparentAddressMetadata> = (0..GAP_LIMIT)
.map(|i| ephemeral::metadata(NonHardenedChildIndex::from_index(i).unwrap()))
.collect();
let actual_metadata: Vec<TransparentAddressMetadata> =
known_addrs.into_iter().map(|(_, meta)| meta).collect();
assert_eq!(actual_metadata, expected_metadata);
};

// The migration should initialize `ephemeral_addresses`.
init_wallet_db_internal(
&mut db_data,
Some(Secret::new(seed0)),
&[super::MIGRATION_ID],
false,
)
.unwrap();
check(&db_data, account0_id);

// Creating a new account should initialize `ephemeral_addresses` for that account.
let seed1 = vec![0x01; 32];
let (account1_id, _usk) =
create_account(&mut db_data, &Secret::new(seed1), &birthday).unwrap();
assert_ne!(account0_id, account1_id);
check(&db_data, account1_id);
}
}
57 changes: 56 additions & 1 deletion zcash_client_sqlite/src/wallet/transparent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -900,7 +900,19 @@ pub(crate) fn queue_transparent_spend_detection<P: consensus::Parameters>(

#[cfg(test)]
mod tests {
use crate::testing::{db::TestDbFactory, BlockCache};
use secrecy::Secret;
use transparent::keys::NonHardenedChildIndex;
use zcash_client_backend::{
data_api::{testing::TestBuilder, Account as _, WalletWrite, GAP_LIMIT},
wallet::TransparentAddressMetadata,
};
use zcash_primitives::block::BlockHash;

use crate::{
testing::{db::TestDbFactory, BlockCache},
wallet::{get_account_ref, transparent::ephemeral},
WalletDb,
};

#[test]
fn put_received_transparent_utxo() {
Expand All @@ -924,4 +936,47 @@ mod tests {
BlockCache::new(),
);
}

#[test]
fn ephemeral_address_management() {
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory::default())
.with_block_cache(BlockCache::new())
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();

let birthday = st.test_account().unwrap().birthday().clone();
let account0_uuid = st.test_account().unwrap().account().id();
let account0_id = get_account_ref(&st.wallet().db().conn, account0_uuid).unwrap();

let check = |db: &WalletDb<_, _>, account_id| {
eprintln!("checking {account_id:?}");
assert_matches!(ephemeral::first_unstored_index(&db.conn, account_id), Ok(addr_index) if addr_index == GAP_LIMIT);
assert_matches!(ephemeral::first_unreserved_index(&db.conn, account_id), Ok(addr_index) if addr_index == 0);

let known_addrs =
ephemeral::get_known_ephemeral_addresses(&db.conn, &db.params, account_id, None)
.unwrap();

let expected_metadata: Vec<TransparentAddressMetadata> = (0..GAP_LIMIT)
.map(|i| ephemeral::metadata(NonHardenedChildIndex::from_index(i).unwrap()))
.collect();
let actual_metadata: Vec<TransparentAddressMetadata> =
known_addrs.into_iter().map(|(_, meta)| meta).collect();
assert_eq!(actual_metadata, expected_metadata);
};

check(st.wallet().db(), account0_id);

// Creating a new account should initialize `ephemeral_addresses` for that account.
let seed1 = vec![0x01; 32];
let (account1_uuid, _usk) = st
.wallet_mut()
.db_mut()
.create_account("test1", &Secret::new(seed1), &birthday, None)
.unwrap();
let account1_id = get_account_ref(&st.wallet().db().conn, account1_uuid).unwrap();
assert_ne!(account0_id, account1_id);
check(st.wallet().db(), account1_id);
}
}

0 comments on commit f48f72b

Please sign in to comment.