zakura-client-sqlite 0.1.0-rc0

An SQLite-based Zcash light client
Documentation
//! This migration adds a birthday height to each account record.

use std::collections::HashSet;

use schemerz_rusqlite::RusqliteMigration;
use uuid::Uuid;
use zcash_protocol::consensus::{self, NetworkUpgrade};

use crate::wallet::init::WalletMigrationError;

use super::shardtree_support;

/// This migration adds a birthday height to each account record.
pub const MIGRATION_ID: Uuid = Uuid::from_u128(0xeeec0d0d_fee0_4231_8c68_5f3a7c7c2245);

const DEPENDENCIES: &[Uuid] = &[shardtree_support::MIGRATION_ID];

pub(super) struct Migration<P> {
    pub(super) params: P,
}

impl<P> schemerz::Migration<Uuid> for Migration<P> {
    fn id(&self) -> Uuid {
        MIGRATION_ID
    }

    fn dependencies(&self) -> HashSet<Uuid> {
        DEPENDENCIES.iter().copied().collect()
    }

    fn description(&self) -> &'static str {
        "Adds a birthday height for each account."
    }
}

impl<P: consensus::Parameters> RusqliteMigration for Migration<P> {
    type Error = WalletMigrationError;

    fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), Self::Error> {
        transaction.execute_batch(&format!(
            "ALTER TABLE accounts ADD COLUMN birthday_height INTEGER;

            -- set the birthday height to the height of the first block in the blocks table
            UPDATE accounts SET birthday_height = MIN(blocks.height) FROM blocks;
            -- if the blocks table is empty, set the birthday height to Sapling activation - 1
            UPDATE accounts SET birthday_height = {} WHERE birthday_height IS NULL;

            CREATE TABLE accounts_new (
                account INTEGER PRIMARY KEY,
                ufvk TEXT NOT NULL,
                birthday_height INTEGER NOT NULL,
                recover_until_height INTEGER
            );

            INSERT INTO accounts_new (account, ufvk, birthday_height)
            SELECT account, ufvk, birthday_height FROM accounts;

            PRAGMA legacy_alter_table = ON;
            DROP TABLE accounts;
            ALTER TABLE accounts_new RENAME TO accounts;
            PRAGMA legacy_alter_table = OFF;",
            // Sapling might not be active in regtest mode, in which case we fall back to
            // the genesis block.
            self.params
                .activation_height(NetworkUpgrade::Sapling)
                .map(u32::from)
                .unwrap_or(0),
        ))?;

        Ok(())
    }

    fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), Self::Error> {
        Err(WalletMigrationError::CannotRevert(MIGRATION_ID))
    }
}

#[cfg(test)]
mod tests {
    use rusqlite::named_params;
    use secrecy::Secret;
    use tempfile::NamedTempFile;
    use zcash_keys::keys::UnifiedSpendingKey;
    use zcash_protocol::consensus::Network;
    use zip32::AccountId;

    use super::{DEPENDENCIES, MIGRATION_ID};
    use crate::{
        WalletDb,
        testing::db::{test_clock, test_rng},
        wallet::init::WalletMigrator,
    };

    #[test]
    fn migrate() {
        let data_file = NamedTempFile::new().unwrap();
        let network = Network::TestNetwork;
        let mut db_data =
            WalletDb::for_path(data_file.path(), network, test_clock(), test_rng()).unwrap();

        let seed_bytes = vec![0xab; 32];
        WalletMigrator::new()
            .with_seed(Secret::new(seed_bytes.clone()))
            .ignore_seed_relevance()
            .init_or_migrate_to(&mut db_data, DEPENDENCIES)
            .unwrap();

        let usk =
            UnifiedSpendingKey::from_seed(&network, &seed_bytes[..], AccountId::ZERO).unwrap();
        let ufvk_str = usk.to_unified_full_viewing_key().encode(&network);

        db_data
            .conn
            .execute(
                "INSERT INTO accounts (account, ufvk) VALUES (0, :ufvk_str)",
                named_params![":ufvk_str": ufvk_str],
            )
            .unwrap();
        db_data
            .conn
            .execute_batch(
                "INSERT INTO addresses (account, diversifier_index_be, address) 
                VALUES (0, X'', 'not_a_real_address');",
            )
            .unwrap();

        WalletMigrator::new()
            .with_seed(Secret::new(seed_bytes))
            .ignore_seed_relevance()
            .init_or_migrate_to(&mut db_data, &[MIGRATION_ID])
            .unwrap();
    }
}