zakura-client-sqlite 0.1.0-rc9.2

An SQLite-based Zcash light client
//! The account's transparent ledger snapshot: authority, authorized balance, last-known amount
//! and blockers, read together.

use zcash_client_backend::data_api::{
    transparent_ledger::{
        ChainPoint, LastKnownBalance, LastKnownSource, RecoveryBlocker, RecoveryCompletion,
        TransparentAuthority, TransparentLedgerBalance, TransparentLedgerMode,
        TransparentLedgerSnapshot,
    },
    wallet::{ConfirmationsPolicy, TargetHeight},
};
use zcash_protocol::{
    consensus::{self, BlockHeight},
    value::Zatoshis,
};

use crate::{AccountUuid, error::SqliteClientError, wallet::chain_tip_height};

#[cfg(feature = "transparent-inputs")]
use {
    super::recovery,
    zcash_client_backend::data_api::{AccountBalance, transparent_ledger::AccountLifecycle},
    zcash_keys::keys::transparent::gap_limits::GapLimits,
};

/// Reads the account's transparent balance, counting only ledger-projected outputs when
/// `ledger_only`. Without transparent support this build cannot read transparent state that
/// another build may have written, so it reports none rather than a zero balance.
#[cfg(feature = "transparent-inputs")]
fn transparent_balance(
    nu7_activation_height: Option<zcash_protocol::consensus::BlockHeight>,
    conn: &rusqlite::Connection,
    account: AccountUuid,
    target_height: TargetHeight,
    confirmations_policy: ConfirmationsPolicy,
    ledger_only: bool,
) -> Result<Option<TransparentLedgerBalance>, SqliteClientError> {
    let mut balances = std::collections::HashMap::<AccountUuid, AccountBalance>::new();
    crate::wallet::transparent::add_transparent_account_balances(
        nu7_activation_height,
        conn,
        target_height,
        confirmations_policy,
        ledger_only,
        Some(account),
        &mut balances,
    )?;
    let balance = balances.remove(&account).unwrap_or(AccountBalance::ZERO);
    Ok(Some(TransparentLedgerBalance {
        regular: *balance.unshielded_regular_balance(),
        coinbase: *balance.unshielded_coinbase_balance(),
    }))
}

#[cfg(not(feature = "transparent-inputs"))]
fn transparent_balance(
    _nu7_activation_height: Option<zcash_protocol::consensus::BlockHeight>,
    _: &rusqlite::Connection,
    _: AccountUuid,
    _: TargetHeight,
    _: ConfirmationsPolicy,
    _: bool,
) -> Result<Option<TransparentLedgerBalance>, SqliteClientError> {
    Ok(None)
}

/// The private ledger's part of a snapshot.
struct PrivateLedgerView {
    authority: Option<TransparentLedgerBalance>,
    last_known: Option<LastKnownBalance>,
    blockers: Vec<RecoveryBlocker>,
    covered_through: Option<ChainPoint>,
    recovered_unverified: Option<Zatoshis>,
}

/// Reads `account`'s private ledger for a `PrivateRequired` snapshot targeting the block after
/// `tip`.
#[cfg(feature = "transparent-inputs")]
fn private_ledger_view<P: consensus::Parameters>(
    _nu7_activation_height: Option<zcash_protocol::consensus::BlockHeight>,
    conn: &rusqlite::Connection,
    params: &P,
    gap_limits: &GapLimits,
    account: AccountUuid,
    tip: BlockHeight,
    confirmations_policy: ConfirmationsPolicy,
) -> Result<PrivateLedgerView, SqliteClientError> {
    let ledger = recovery::account_ledger(conn, params, gap_limits, account)?;
    let target = TargetHeight::from(tip + 1);
    let covered_through = match ledger.status.covered_through {
        Some(height) => {
            crate::wallet::get_block_hash(conn, height)?.map(|hash| ChainPoint { height, hash })
        }
        None => None,
    };
    let (authority, last_known, blockers) = if ledger.authorizes_after(tip) {
        (
            transparent_balance(
                zcash_protocol::consensus::Parameters::activation_height(
                    params,
                    zcash_protocol::consensus::NetworkUpgrade::Nu7,
                ),
                conn,
                account,
                target,
                confirmations_policy,
                true,
            )?,
            None,
            vec![],
        )
    } else {
        let last_known = match ledger.lifecycle {
            // An active account's own ledger is the best prior amount. It is evaluated after
            // the tip, so it is established at the covered point only when coverage reaches the
            // tip; while coverage lags, placed events above it may already count.
            AccountLifecycle::Active => {
                let at = covered_through.filter(|point| point.height == tip);
                transparent_balance(
                    zcash_protocol::consensus::Parameters::activation_height(
                        params,
                        zcash_protocol::consensus::NetworkUpgrade::Nu7,
                    ),
                    conn,
                    account,
                    target,
                    confirmations_policy,
                    true,
                )?
                .map(|balance| LastKnownBalance {
                    balance,
                    source: LastKnownSource::PrivateLedger,
                    at,
                })
            }
            AccountLifecycle::Candidate => legacy_last_known(
                zcash_protocol::consensus::Parameters::activation_height(
                    params,
                    zcash_protocol::consensus::NetworkUpgrade::Nu7,
                ),
                conn,
                account,
                target,
                confirmations_policy,
            )?,
        };
        (
            None,
            last_known,
            recovery::ledger_blockers(conn, &ledger, Some(tip))?,
        )
    };
    Ok(PrivateLedgerView {
        authority,
        last_known,
        blockers,
        covered_through,
        recovered_unverified: ledger.status.recovered_unverified,
    })
}

/// Without transparent support there is no private ledger to read.
#[cfg(not(feature = "transparent-inputs"))]
fn private_ledger_view(
    nu7_activation_height: Option<zcash_protocol::consensus::BlockHeight>,
    conn: &rusqlite::Connection,
    account: AccountUuid,
    tip: BlockHeight,
    confirmations_policy: ConfirmationsPolicy,
) -> Result<PrivateLedgerView, SqliteClientError> {
    Ok(PrivateLedgerView {
        authority: None,
        last_known: legacy_last_known(
            nu7_activation_height,
            conn,
            account,
            TargetHeight::from(tip + 1),
            confirmations_policy,
        )?,
        blockers: vec![
            RecoveryBlocker::TransparentSupportUnavailable,
            RecoveryBlocker::PrivateRecoveryUnavailable,
        ],
        covered_through: None,
        recovered_unverified: None,
    })
}

/// The account's whole transparent balance as last-known evidence, classified by provenance.
fn legacy_last_known(
    nu7_activation_height: Option<zcash_protocol::consensus::BlockHeight>,
    conn: &rusqlite::Connection,
    account: AccountUuid,
    target: TargetHeight,
    confirmations_policy: ConfirmationsPolicy,
) -> Result<Option<LastKnownBalance>, SqliteClientError> {
    transparent_balance(
        nu7_activation_height,
        conn,
        account,
        target,
        confirmations_policy,
        false,
    )?
    .map(|balance| {
        Ok(LastKnownBalance {
            balance,
            source: last_known_source(
                nu7_activation_height,
                conn,
                account,
                target,
                confirmations_policy,
            )?,
            at: None,
        })
    })
    .transpose()
}

/// Builds the snapshot for `account` from the connection's current state. The caller provides
/// the read transaction.
pub(crate) fn ledger_snapshot<P: consensus::Parameters>(
    conn: &rusqlite::Connection,
    #[cfg_attr(not(feature = "transparent-inputs"), allow(unused_variables))] params: &P,
    #[cfg(feature = "transparent-inputs")] gap_limits: &GapLimits,
    account: AccountUuid,
    mode: TransparentLedgerMode,
    confirmations_policy: ConfirmationsPolicy,
) -> Result<TransparentLedgerSnapshot<AccountUuid>, SqliteClientError> {
    let account_exists: bool = conn.query_row(
        "SELECT EXISTS (SELECT 1 FROM accounts WHERE uuid = ?1)",
        [account.0],
        |row| row.get(0),
    )?;
    if !account_exists {
        return Err(SqliteClientError::AccountUnknown);
    }

    let blocked_snapshot = |blockers| TransparentLedgerSnapshot {
        account,
        mode,
        authority: TransparentAuthority::Unavailable,
        authorized: None,
        last_known: None,
        completion: RecoveryCompletion::Blocked,
        blockers,
        covered_through: None,
        recovered_unverified: None,
    };
    // Authority cannot be established before the chain is known.
    let Some(tip) = chain_tip_height(conn)? else {
        return Ok(blocked_snapshot(vec![RecoveryBlocker::ChainUnknown]));
    };
    let target = TargetHeight::from(tip + 1);

    if mode.retains_public_authority() {
        // A build that cannot read transparent state never reports public authority.
        let Some(balance) = transparent_balance(
            zcash_protocol::consensus::Parameters::activation_height(
                params,
                zcash_protocol::consensus::NetworkUpgrade::Nu7,
            ),
            conn,
            account,
            target,
            confirmations_policy,
            false,
        )?
        else {
            return Ok(blocked_snapshot(vec![
                RecoveryBlocker::TransparentSupportUnavailable,
            ]));
        };
        return Ok(TransparentLedgerSnapshot {
            account,
            mode,
            authority: TransparentAuthority::Public,
            authorized: Some(balance),
            last_known: None,
            completion: RecoveryCompletion::NotApplicable,
            blockers: vec![],
            covered_through: None,
            recovered_unverified: None,
        });
    }

    #[cfg(feature = "transparent-inputs")]
    let private_ledger = private_ledger_view(
        zcash_protocol::consensus::Parameters::activation_height(
            params,
            zcash_protocol::consensus::NetworkUpgrade::Nu7,
        ),
        conn,
        params,
        gap_limits,
        account,
        tip,
        confirmations_policy,
    )?;
    #[cfg(not(feature = "transparent-inputs"))]
    let private_ledger = private_ledger_view(
        zcash_protocol::consensus::Parameters::activation_height(
            params,
            zcash_protocol::consensus::NetworkUpgrade::Nu7,
        ),
        conn,
        account,
        tip,
        confirmations_policy,
    )?;
    let (covered_through, recovered_unverified) = (
        private_ledger.covered_through,
        private_ledger.recovered_unverified,
    );
    Ok(match private_ledger.authority {
        Some(balance) => TransparentLedgerSnapshot {
            account,
            mode,
            authority: TransparentAuthority::Private,
            authorized: Some(balance),
            last_known: None,
            completion: RecoveryCompletion::Complete,
            blockers: vec![],
            covered_through,
            recovered_unverified,
        },
        None => TransparentLedgerSnapshot {
            account,
            mode,
            authority: TransparentAuthority::Unavailable,
            authorized: None,
            last_known: private_ledger.last_known,
            completion: RecoveryCompletion::Blocked,
            blockers: private_ledger.blockers,
            covered_through,
            recovered_unverified,
        },
    })
}

/// Classifies the provenance of the outputs counted in the account's transparent balance.
#[cfg(feature = "transparent-inputs")]
fn last_known_source(
    nu7_activation_height: Option<zcash_protocol::consensus::BlockHeight>,
    conn: &rusqlite::Connection,
    account: AccountUuid,
    target_height: TargetHeight,
    confirmations_policy: ConfirmationsPolicy,
) -> Result<LastKnownSource, SqliteClientError> {
    use crate::wallet::transparent::{BalanceProvenance, transparent_balance_provenance};
    Ok(
        match transparent_balance_provenance(
            nu7_activation_height,
            conn,
            account,
            target_height,
            confirmations_policy,
        )? {
            BalanceProvenance::LegacyPublic => LastKnownSource::LegacyPublic,
            BalanceProvenance::IncludesLocalOnly => LastKnownSource::LegacyPublicAndLocal,
        },
    )
}

/// Without transparent support no balance is read, so no last-known amount is classified.
#[cfg(not(feature = "transparent-inputs"))]
fn last_known_source(
    _nu7_activation_height: Option<zcash_protocol::consensus::BlockHeight>,
    _: &rusqlite::Connection,
    _: AccountUuid,
    _: TargetHeight,
    _: ConfirmationsPolicy,
) -> Result<LastKnownSource, SqliteClientError> {
    Ok(LastKnownSource::LegacyPublic)
}