use rusqlite::{OptionalExtension as _, named_params};
use zcash_client_backend::data_api::transparent_ledger::{
AppliedTransparentPolicy, PrivateTransparentDetail, TransparentLedgerMode,
};
use zcash_primitives::transaction::TxId;
use crate::{error::SqliteClientError, wallet::init::migrations::TRANSPARENT_LEDGER_SCHEMA_ID};
use super::{reader_version::TPIR_READER_VERSION, transactions::in_transaction};
fn mode_from_code(code: i64) -> Result<TransparentLedgerMode, SqliteClientError> {
match code {
0 => Ok(TransparentLedgerMode::Public),
2 => Ok(TransparentLedgerMode::PrivateRequired),
other => Err(SqliteClientError::CorruptedData(format!(
"unknown transparent ledger mode code {other}"
))),
}
}
fn mode_code(mode: TransparentLedgerMode) -> i64 {
match mode {
TransparentLedgerMode::Public => 0,
TransparentLedgerMode::PrivateRequired => 2,
}
}
fn missing_policy_row() -> SqliteClientError {
SqliteClientError::CorruptedData("tpir_meta policy row is missing".into())
}
pub(crate) fn durable_policy(
conn: &rusqlite::Connection,
) -> Result<Option<AppliedTransparentPolicy>, SqliteClientError> {
let has_meta: bool = conn.query_row(
"SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'tpir_meta')",
[],
|row| row.get(0),
)?;
if !has_meta {
let migrated: bool = conn.query_row(
"SELECT EXISTS (SELECT 1 FROM schemer_migrations WHERE id = ?1)",
[TRANSPARENT_LEDGER_SCHEMA_ID.as_bytes().to_vec()],
|row| row.get(0),
)?;
return if migrated {
Err(SqliteClientError::CorruptedData(
"tpir_meta is missing after the transparent ledger migration".into(),
))
} else {
Ok(None)
};
}
let (mode, generation, min_reader_version) = conn
.query_row(
"SELECT applied_mode, policy_generation, min_reader_version FROM tpir_meta WHERE id = 0",
[],
|row| {
Ok((
row.get::<_, i64>(0)?,
row.get::<_, i64>(1)?,
row.get::<_, i64>(2)?,
))
},
)
.optional()?
.ok_or_else(missing_policy_row)?;
if min_reader_version > TPIR_READER_VERSION {
return Err(SqliteClientError::TransparentLedgerIncompatible {
required: min_reader_version,
});
}
Ok(Some(AppliedTransparentPolicy {
mode: mode_from_code(mode)?,
generation: u64::try_from(generation).map_err(|_| {
SqliteClientError::CorruptedData("negative policy_generation in tpir_meta".into())
})?,
}))
}
fn effective_mode(
configured: TransparentLedgerMode,
durable: Option<AppliedTransparentPolicy>,
) -> TransparentLedgerMode {
match durable {
Some(AppliedTransparentPolicy {
mode: TransparentLedgerMode::PrivateRequired,
..
}) => TransparentLedgerMode::PrivateRequired,
_ => configured,
}
}
pub(crate) fn resolve_mode(
conn: &rusqlite::Connection,
configured: Option<TransparentLedgerMode>,
) -> Result<TransparentLedgerMode, SqliteClientError> {
let mode = configured.ok_or(SqliteClientError::TransparentLedgerModeNotConfigured)?;
Ok(effective_mode(mode, durable_policy(conn)?))
}
pub(crate) fn applied_transparent_policy(
conn: &rusqlite::Connection,
configured: Option<TransparentLedgerMode>,
) -> Result<AppliedTransparentPolicy, SqliteClientError> {
let _ = resolve_mode(conn, configured)?;
required_policy(conn)
}
pub(crate) fn check_transparent_policy_generation(
conn: &rusqlite::Connection,
expected: u64,
) -> Result<(), SqliteClientError> {
let applied = required_policy(conn)?;
if applied.generation != expected {
return Err(SqliteClientError::StaleTransparentPolicy {
expected,
applied: applied.generation,
});
}
Ok(())
}
pub(crate) fn apply_transparent_policy(
conn: &rusqlite::Connection,
configured: Option<TransparentLedgerMode>,
mode: TransparentLedgerMode,
) -> Result<AppliedTransparentPolicy, SqliteClientError> {
let _ = configured.ok_or(SqliteClientError::TransparentLedgerModeNotConfigured)?;
in_transaction(conn, |conn| transition_policy(conn, mode))
}
fn transition_policy(
conn: &rusqlite::Connection,
mode: TransparentLedgerMode,
) -> Result<AppliedTransparentPolicy, SqliteClientError> {
let current = required_policy(conn)?;
if current.mode == mode {
return Ok(current);
}
let generation = current
.generation
.checked_add(1)
.ok_or_else(|| SqliteClientError::CorruptedData("policy_generation overflow".into()))?;
let updated = conn.execute(
"UPDATE tpir_meta
SET applied_mode = :mode,
policy_generation = :generation
WHERE id = 0",
rusqlite::named_params![
":mode": mode_code(mode),
":generation": i64::try_from(generation).map_err(|_| {
SqliteClientError::CorruptedData("policy_generation does not fit i64".into())
})?,
],
)?;
if updated != 1 {
return Err(missing_policy_row());
}
super::clear_pending_pages(conn)?;
if current.mode == TransparentLedgerMode::PrivateRequired {
conn.execute("DELETE FROM tpir_active_accounts", [])?;
}
let generation_i64 = i64::try_from(generation).map_err(|_| {
SqliteClientError::CorruptedData("policy_generation does not fit i64".into())
})?;
conn.execute(
"UPDATE tx_retrieval_queue SET policy_generation = :generation",
rusqlite::named_params![":generation": generation_i64],
)?;
if mode == TransparentLedgerMode::PrivateRequired {
conn.execute(
"UPDATE ironwood_enhance_routing
SET route = 2
WHERE route = 1
AND EXISTS (
SELECT 1 FROM transactions t
WHERE t.id_tx = ironwood_enhance_routing.transaction_id
AND t.raw IS NULL
)",
[],
)?;
super::details::enqueue_route_two(conn)?;
#[cfg(feature = "orchard")]
crate::wallet::enhance_pir::queue_unsupported_memos(conn, None)?;
} else if mode.retains_public_authority() {
conn.execute(
"UPDATE ironwood_enhance_routing
SET route = 1
WHERE route = 2
AND EXISTS (
SELECT 1 FROM transactions t
WHERE t.id_tx = ironwood_enhance_routing.transaction_id
AND t.raw IS NULL
)",
[],
)?;
conn.execute(
"DELETE FROM ironwood_enhance_metadata_queue WHERE transaction_id IN (
SELECT transaction_id FROM ironwood_enhance_routing WHERE route = 1)",
[],
)?;
conn.execute(
"DELETE FROM ironwood_memo_retrieval_queue
WHERE received_note_id IN (
SELECT rn.id FROM ironwood_received_notes rn
JOIN ironwood_enhance_routing r ON r.transaction_id = rn.transaction_id
WHERE r.route = 1
)",
[],
)?;
conn.execute(
"INSERT INTO tx_retrieval_queue (txid, query_type, policy_generation)
SELECT t.txid, 1, :generation
FROM ironwood_enhance_routing r
JOIN transactions t ON t.id_tx = r.transaction_id
WHERE r.route = 1 AND t.raw IS NULL
ON CONFLICT (txid, query_type) DO NOTHING",
rusqlite::named_params![":generation": generation_i64],
)?;
}
Ok(AppliedTransparentPolicy { mode, generation })
}
pub(crate) fn pending_private_transparent_details(
conn: &rusqlite::Connection,
configured: Option<TransparentLedgerMode>,
) -> Result<Vec<PrivateTransparentDetail>, SqliteClientError> {
pending_details(conn, resolve_mode(conn, configured)?, None)
}
pub(super) fn pending_details(
conn: &rusqlite::Connection,
mode: TransparentLedgerMode,
transaction: Option<i64>,
) -> Result<Vec<PrivateTransparentDetail>, SqliteClientError> {
if mode.retains_public_authority() {
return Ok(vec![]);
}
let mut details = Vec::new();
let mut parents = conn.prepare_cached(
"SELECT q.txid FROM tx_retrieval_queue q
WHERE q.query_type = 1
AND q.dependent_transaction_id IS NOT NULL
AND (
:transaction IS NULL
OR q.dependent_transaction_id = :transaction
OR q.txid IN (
SELECT m.prevout_txid FROM transparent_spend_map m
WHERE m.spending_transaction_id = :transaction
)
)
ORDER BY q.txid",
)?;
for txid in parents.query_map(named_params![":transaction": transaction], |row| {
row.get::<_, [u8; 32]>(0)
})? {
details.push(PrivateTransparentDetail::ParentTransaction {
txid: TxId::from_bytes(txid?),
});
}
let mut mixed = conn.prepare_cached(
"SELECT t.txid FROM ironwood_enhance_routing r
JOIN transactions t ON t.id_tx = r.transaction_id
WHERE r.route IN (1, 2) AND t.raw IS NULL
AND (:transaction IS NULL OR r.transaction_id = :transaction)
ORDER BY t.txid",
)?;
for txid in mixed.query_map(named_params![":transaction": transaction], |row| {
row.get::<_, [u8; 32]>(0)
})? {
details.push(PrivateTransparentDetail::MixedTransaction {
txid: TxId::from_bytes(txid?),
});
}
Ok(details)
}
fn required_policy(
conn: &rusqlite::Connection,
) -> Result<AppliedTransparentPolicy, SqliteClientError> {
durable_policy(conn)?.ok_or_else(missing_policy_row)
}
pub(crate) fn capture_policy_generation(
conn: &rusqlite::Connection,
) -> Result<u64, SqliteClientError> {
Ok(required_policy(conn)?.generation)
}
pub(crate) fn retains_public_authority(
conn: &rusqlite::Connection,
configured: Option<TransparentLedgerMode>,
) -> Result<bool, SqliteClientError> {
match configured {
Some(_) => Ok(resolve_mode(conn, configured)?.retains_public_authority()),
None => Ok(durable_policy(conn)?
.map(|p| p.mode.retains_public_authority())
.unwrap_or(true)),
}
}