zakura-client-sqlite 0.1.0-rc9.2

An SQLite-based Zcash light client
use super::*;
use rusqlite::params;
use zcash_primitives::transaction::TxId;

/// Looking a key up derives and checks only that key, against its stored receiver and
/// the parent of its own account, and registration never replaces a corrupted receiver.
#[test]
fn key_lookups_validate_only_the_requested_key() {
    let mut st = wallet(false);
    let account = st.test_account().unwrap().id();
    let parent = parent(&st);
    let db = st.wallet_mut().db_mut();
    let wanted = reserve_key(db, account, Purpose::Refund, start());
    let corrupted = KeyId::new(Purpose::Receive, 0);
    let lookup = Discovery::Lookup;
    tx(db, |c, p| {
        register(c, p, account, corrupted, start(), false, lookup, 0)
    })
    .unwrap();
    let (account_ref, _) = account_key(db.conn.borrow(), &db.params, account).unwrap();

    // Synthetic metadata avoids 10,000 KDFs in the fixture. These invalid receivers
    // also make any accidental reconstruction of unrelated keys fail immediately.
    tx(db, |c, _| {
        let mut insert = c.prepare(
            "INSERT INTO ironwood_one_time_keys
            (account_id,purpose,key_index,receiver,scan_from,advances_allocation)
            VALUES(?1,0,?2,zeroblob(43),100,1)",
        )?;
        for index in 1u64..=10_000 {
            insert.execute(params![account_ref.0, index.to_be_bytes()])?;
        }
        Ok(())
    })
    .unwrap();
    let sql = "UPDATE ironwood_one_time_keys SET receiver = zeroblob(43) WHERE purpose = 1";
    exec(&db.conn, sql, []);
    for found in [
        get_key(db, account, wanted.key_id()).unwrap().unwrap(),
        db.get_one_time_key_for_receiver(account, &wanted.receiver())
            .unwrap()
            .unwrap(),
    ] {
        assert_eq!(found.key_id(), wanted.key_id());
        assert_eq!(
            found.full_viewing_key().to_bytes(),
            wanted.full_viewing_key().to_bytes()
        );
    }
    let absent = KeyId::new(Purpose::Refund, 10_001);
    assert!(get_key(db, account, absent).unwrap().is_none());
    assert!(get_key(db, account, KeyId::new(Purpose::Refund, 1)).is_err());
    assert!(matches!(
        all_keys(db, account),
        Err(SqliteClientError::CorruptedData(_))
    ));
    // Repeated recovery neither replaces the corrupted receiver nor promotes its key.
    let registered = tx(db, |c, p| {
        register(c, p, account, corrupted, start(), true, lookup, 0)
    });
    assert!(matches!(
        registered,
        Err(SqliteClientError::CorruptedData(_))
    ));
    assert!(!key_state(db, corrupted).1);

    // A note's key is re-derived only from its own account's parent.
    let id = key_ref(&db.conn, account, wanted.key_id()).unwrap();
    assert_eq!(note_key(&db.conn, id, &parent).unwrap().0, wanted.key_id());
    let other = FullViewingKey::from(&orchard::keys::SpendingKey::from_bytes([9; 32]).unwrap());
    assert!(note_key(&db.conn, id, &other).is_err());
    // Selecting an existing receiver must still validate the stored key identity.
    exec(
        &db.conn,
        "UPDATE ironwood_one_time_keys SET key_index=?1 WHERE account_id=?2 AND key_index=?3",
        params![20_000u64.to_be_bytes(), account_ref.0, 0u64.to_be_bytes()],
    );
    let found = db.get_one_time_key_for_receiver(account, &wanted.receiver());
    assert!(found.is_err());
    assert!(note_key(&db.conn, id, &parent).is_err());
}

#[test]
fn transaction_keys_exclude_unscanned_history_and_preserve_self_payments() {
    let mut st = wallet(false);
    let account = st.test_account().unwrap().id();
    let db = st.wallet_mut().db_mut();
    let historical = recover(db, account, KeyId::new(Purpose::Refund, 70), start());
    let active = reserve_key(db, account, Purpose::Refund, start());
    let txid = TxId::from_bytes([17; 32]);
    let keys = db
        .get_one_time_transaction_keys(txid, Some(start()), &[])
        .unwrap();
    assert_eq!(keys.len(), 1);
    assert_eq!(keys[0].key_id(), active.key_id());
    // An ordinary OVK can identify a self-payment before compact scanning, even
    // when the one-time key was recovered without being scanned.
    let keys = db
        .get_one_time_transaction_keys(txid, None, &[historical.receiver()])
        .unwrap();
    assert_eq!(keys.len(), 2);
    assert!(keys.iter().any(|k| k.key_id() == historical.key_id()));
    // A closed key stops being tried on unrelated transactions.
    let sql =
        "UPDATE ironwood_one_time_keys SET closed_at = 1 WHERE purpose = 0 AND key_index = ?1";
    exec(&db.conn, sql, [active.key_id().index().to_be_bytes()]);
    let keys = db.get_one_time_transaction_keys(txid, Some(start()), &[]);
    assert!(keys.unwrap().is_empty());
}

#[test]
fn unscanned_transaction_keys_follow_pending_and_imported_output_identity() {
    use store::restore::tests::discovery::{key8, late_fixture, send_witness};
    let (mut st, candidate, publication) = late_fixture();
    let account = st.test_account().unwrap().id();
    let db = st.wallet_mut().db_mut();
    assert!(db.get_one_time_scanning_keys().unwrap().is_empty());
    let keys_for = |db: &Db, txid| {
        let keys = db.get_one_time_transaction_keys(txid, Some(publication.height), &[]);
        keys.unwrap().iter().map(|k| k.key_id()).collect::<Vec<_>>()
    };
    assert_eq!(keys_for(db, candidate.txid), [key8()]);
    assert!(keys_for(db, TxId::from_bytes([45; 32])).is_empty());
    send_witness(&mut st, key8(), &candidate);
    let db = st.wallet_mut().db_mut();
    assert!(pending(db, account, key8()).is_empty());
    assert_eq!(keys_for(db, candidate.txid), [key8()]);
}