use std::collections::{BTreeMap, BTreeSet};
use rusqlite::{Connection, OptionalExtension as _, named_params};
use transparent::address::TransparentAddress;
use zcash_client_backend::data_api::transparent_ledger::{
TransparentDisplayContradiction, TransparentDisplayFacts, TransparentDisplaySender,
transparent_display_address,
};
use zcash_protocol::value::Zatoshis;
use crate::error::SqliteClientError;
use super::super::output_observation_condition;
#[derive(Default)]
struct KnownInput {
input_index: Option<u32>,
script: Option<Vec<u8>>,
value: Option<u64>,
}
fn known_inputs(
conn: &Connection,
id_tx: i64,
txid: &[u8],
) -> Result<BTreeMap<([u8; 32], u32), KnownInput>, SqliteClientError> {
let counted = output_observation_condition("o");
let mut inputs: BTreeMap<([u8; 32], u32), KnownInput> = BTreeMap::new();
let mut stmt = conn.prepare_cached(&format!(
"SELECT e.prevout_txid, e.prevout_output_index, e.input_index, e.prevout_script,
(SELECT re.value_zat FROM tpir_receive_events re
WHERE re.txid = e.prevout_txid AND re.output_index = e.prevout_output_index)
FROM tpir_spend_events e
WHERE e.spending_txid = :txid
UNION ALL
SELECT ot.txid, o.output_index, NULL,
CASE WHEN ({counted}) THEN o.script END,
CASE WHEN ({counted}) THEN o.value_zat END
FROM transparent_received_output_spends s
JOIN transparent_received_outputs o ON o.id = s.transparent_received_output_id
JOIN transactions ot ON ot.id_tx = o.transaction_id
WHERE s.transaction_id = :tx
UNION ALL
SELECT m.prevout_txid, m.prevout_output_index, NULL, NULL, NULL
FROM transparent_spend_map m
WHERE m.spending_transaction_id = :tx"
))?;
let mut rows = stmt.query(named_params![":tx": id_tx, ":txid": txid])?;
while let Some(row) = rows.next()? {
let outpoint = (row.get::<_, [u8; 32]>(0)?, row.get::<_, u32>(1)?);
let known = inputs.entry(outpoint).or_default();
known.input_index = known.input_index.or(row.get(2)?);
if known.script.is_none() {
known.script = row.get(3)?;
}
known.value = known.value.or(row.get(4)?);
}
Ok(inputs)
}
fn known_first_address(
inputs: &BTreeMap<([u8; 32], u32), KnownInput>,
count: u32,
) -> Option<Option<TransparentAddress>> {
let by_index: BTreeMap<u32, Option<&[u8]>> = inputs
.values()
.filter_map(|i| i.input_index.map(|index| (index, i.script.as_deref())))
.collect();
for index in 0..count {
let script = (*by_index.get(&index)?)?;
if let Some(address) = transparent_display_address(script) {
return Some(Some(address));
}
}
Some(None)
}
fn sender_contradiction(
facts: &TransparentDisplayFacts,
inputs: &BTreeMap<([u8; 32], u32), KnownInput>,
) -> Option<TransparentDisplayContradiction> {
use TransparentDisplayContradiction as C;
let scripts: BTreeSet<&[u8]> = inputs
.values()
.filter_map(|i| i.script.as_deref())
.collect();
let all_known =
inputs.len() == facts.input_count as usize && inputs.values().all(|i| i.script.is_some());
if (scripts.len() > 1 && !facts.multiple_source_scripts)
|| (all_known && facts.multiple_source_scripts != (scripts.len() > 1))
{
return Some(C::SourceScripts);
}
let addressed: BTreeSet<TransparentAddress> = scripts
.iter()
.filter_map(|s| transparent_display_address(s))
.collect();
let consistent = match (facts.sender, known_first_address(inputs, facts.input_count)) {
(TransparentDisplaySender::Absent, _) => true,
(TransparentDisplaySender::NonStandard, _) => addressed.is_empty(),
(TransparentDisplaySender::Address(sender), Some(first)) => first == Some(sender),
(TransparentDisplaySender::Address(sender), None) if all_known => {
addressed.contains(&sender)
}
(TransparentDisplaySender::Address(sender), None) if !facts.multiple_source_scripts => {
scripts
.first()
.is_none_or(|s| transparent_display_address(s) == Some(sender))
}
(TransparentDisplaySender::Address(_), None) => true,
};
(!consistent).then_some(C::Sender)
}
fn funding_contradiction(
facts: &TransparentDisplayFacts,
inputs: &BTreeMap<([u8; 32], u32), KnownInput>,
) -> Option<TransparentDisplayContradiction> {
if facts.input_count == 0 || inputs.len() != facts.input_count as usize {
return None;
}
let spent: u128 = inputs
.values()
.map(|i| i.value.map(u128::from))
.sum::<Option<u128>>()?;
let paid = u128::from(facts.fee.into_u64())
+ facts
.outputs
.iter()
.map(|o| u128::from(o.value.into_u64()))
.sum::<u128>();
let contradicted = if paid > spent {
!facts.shielded_and_transparent_funding
} else if !facts.more_than_two_outputs() {
facts.shielded_and_transparent_funding || (!facts.shielded_components && paid != spent)
} else {
false
};
contradicted.then_some(TransparentDisplayContradiction::Funding)
}
pub(super) fn validate(
conn: &Connection,
id_tx: i64,
tx_index: Option<u32>,
stored_fee: Option<u64>,
facts: &TransparentDisplayFacts,
) -> Result<Option<TransparentDisplayContradiction>, SqliteClientError> {
use TransparentDisplayContradiction as C;
let txid = facts.txid.as_ref();
let inputs = known_inputs(conn, id_tx, txid)?;
let known_spends = u32::try_from(inputs.len()).unwrap_or(u32::MAX);
let event_coinbase: Vec<bool> = conn
.prepare_cached("SELECT DISTINCT coinbase FROM tpir_receive_events WHERE txid = :txid")?
.query_map(named_params![":txid": txid], |row| row.get(0))?
.collect::<Result<_, _>>()?;
if tx_index.is_some_and(|i| (i == 0) != facts.coinbase)
|| event_coinbase.iter().any(|c| *c != facts.coinbase)
|| (facts.coinbase
&& (facts.fee != Zatoshis::ZERO || facts.input_count != 0 || known_spends > 0))
{
return Ok(Some(C::Coinbase));
}
if !facts.is_well_formed() {
return Ok(Some(C::Malformed));
}
let owned: Vec<(u32, u64, Vec<u8>)> = conn
.prepare_cached(&format!(
"SELECT output_index, value_zat, script FROM transparent_received_outputs o
WHERE transaction_id = :tx AND ({})
UNION
SELECT output_index, value_zat, script FROM tpir_receive_events WHERE txid = :txid
ORDER BY 1",
output_observation_condition("o"),
))?
.query_map(named_params![":tx": id_tx, ":txid": txid], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?))
})?
.collect::<Result<_, _>>()?;
for (index, value, script) in owned {
if index >= facts.output_count {
return Ok(Some(C::OutputIndexOutOfRange { index }));
}
if let Some(given) = facts.outputs.get(index as usize)
&& (given.value.into_u64() != value
|| given.address != transparent_display_address(&script))
{
return Ok(Some(C::OwnedOutput { index }));
}
}
let metadata_conflict: bool = conn.query_row(
"SELECT EXISTS (SELECT 1 FROM tpir_transaction_metadata WHERE txid = :txid
AND (input_count != :inputs OR shielded != :shielded
OR (fee_state = 2) != :coinbase
OR (fee_state = 0 AND fee_zat != :fee)))",
named_params![
":txid": txid,
":inputs": facts.input_count,
":shielded": facts.shielded_components,
":coinbase": facts.coinbase,
":fee": facts.fee.into_u64() as i64,
],
|row| row.get(0),
)?;
if metadata_conflict {
return Ok(Some(C::Metadata));
}
if let Some(stored) = stored_fee
&& (facts.coinbase || facts.fee.into_u64() != stored)
{
return Ok(Some(C::Fee));
}
if !facts.shielded_components {
let shielded: bool = conn.query_row(
"SELECT EXISTS (SELECT 1 FROM sapling_received_notes WHERE transaction_id = :tx)
OR EXISTS (SELECT 1 FROM orchard_received_notes WHERE transaction_id = :tx)
OR EXISTS (SELECT 1 FROM ironwood_received_notes WHERE transaction_id = :tx)
OR EXISTS (SELECT 1 FROM sapling_received_note_spends WHERE transaction_id = :tx)
OR EXISTS (SELECT 1 FROM orchard_received_note_spends WHERE transaction_id = :tx)
OR EXISTS (SELECT 1 FROM ironwood_received_note_spends WHERE transaction_id = :tx)
OR EXISTS (SELECT 1 FROM sent_notes WHERE transaction_id = :tx AND output_pool != 0)
OR EXISTS (SELECT 1 FROM ironwood_enhance_routing
WHERE transaction_id = :tx AND route = 2)",
named_params![":tx": id_tx],
|row| row.get(0),
)?;
if shielded {
return Ok(Some(C::Shielded));
}
}
let beyond: Option<u32> = conn
.query_row(
"SELECT MIN(input_index) FROM tpir_spend_events
WHERE spending_txid = :txid AND input_index >= :inputs",
named_params![":txid": txid, ":inputs": facts.input_count],
|row| row.get(0),
)
.optional()?
.flatten();
if let Some(index) = beyond {
return Ok(Some(C::InputIndex { index }));
}
if known_spends > facts.input_count {
return Ok(Some(C::InputCount {
known: known_spends,
}));
}
Ok(sender_contradiction(facts, &inputs).or_else(|| funding_contradiction(facts, &inputs)))
}