cuprate_consensus/transactions/
contextual_data.rs

1//! # Contextual Data
2//!
3//! This module fills [`TxRingMembersInfo`] which is a struct made up from blockchain information about the
4//! ring members of inputs. This module does minimal consensus checks, only when needed, and should not be relied
5//! upon to do any.
6//!
7//! The data collected by this module can be used to perform consensus checks.
8//!
9//! ## Why not use the context service?
10//!
11//! Because this data is unique for *every* transaction and the context service is just for blockchain state data.
12//!
13
14use std::{borrow::Cow, collections::HashSet};
15
16use indexmap::IndexMap;
17use monero_serai::transaction::{Input, Timelock};
18use tower::ServiceExt;
19use tracing::instrument;
20
21use cuprate_consensus_rules::{
22    transactions::{
23        get_absolute_offsets, insert_ring_member_ids, DecoyInfo, Rings, TransactionError,
24        TxRingMembersInfo,
25    },
26    ConsensusError, HardFork, TxVersion,
27};
28
29use cuprate_types::{
30    blockchain::{BlockchainReadRequest, BlockchainResponse},
31    output_cache::OutputCache,
32    OutputOnChain,
33};
34
35use crate::{transactions::TransactionVerificationData, Database, ExtendedConsensusError};
36
37/// Get the ring members for the inputs from the outputs on the chain.
38///
39/// Will error if `outputs` does not contain the outputs needed.
40fn get_ring_members_for_inputs(
41    get_outputs: impl Fn(u64, u64) -> Option<OutputOnChain>,
42    inputs: &[Input],
43) -> Result<Vec<Vec<OutputOnChain>>, TransactionError> {
44    inputs
45        .iter()
46        .map(|inp| match inp {
47            Input::ToKey {
48                amount,
49                key_offsets,
50                ..
51            } => {
52                let offsets = get_absolute_offsets(key_offsets)?;
53                Ok(offsets
54                    .iter()
55                    .map(|offset| {
56                        get_outputs(amount.unwrap_or(0), *offset)
57                            .ok_or(TransactionError::RingMemberNotFoundOrInvalid)
58                    })
59                    .collect::<Result<_, TransactionError>>()?)
60            }
61            Input::Gen(_) => Err(TransactionError::IncorrectInputType),
62        })
63        .collect::<Result<_, TransactionError>>()
64}
65
66/// Construct a [`TxRingMembersInfo`] struct.
67///
68/// The used outs must be all the ring members used in the transactions inputs.
69pub fn new_ring_member_info(
70    used_outs: Vec<Vec<OutputOnChain>>,
71    decoy_info: Option<DecoyInfo>,
72    tx_version: TxVersion,
73) -> Result<TxRingMembersInfo, TransactionError> {
74    Ok(TxRingMembersInfo {
75        youngest_used_out_height: used_outs
76            .iter()
77            .map(|inp_outs| {
78                inp_outs
79                    .iter()
80                    // the output with the highest height is the youngest
81                    .map(|out| out.height)
82                    .max()
83                    .expect("Input must have ring members")
84            })
85            .max()
86            .expect("Tx must have inputs"),
87        time_locked_outs: used_outs
88            .iter()
89            .flat_map(|inp_outs| {
90                inp_outs
91                    .iter()
92                    .filter_map(|out| match out.time_lock {
93                        Timelock::None => None,
94                        lock => Some(lock),
95                    })
96                    .collect::<Vec<_>>()
97            })
98            .collect(),
99        rings: new_rings(used_outs, tx_version),
100        decoy_info,
101    })
102}
103
104/// Builds the [`Rings`] for the transaction inputs, from the given outputs.
105fn new_rings(outputs: Vec<Vec<OutputOnChain>>, tx_version: TxVersion) -> Rings {
106    match tx_version {
107        TxVersion::RingSignatures => Rings::Legacy(
108            outputs
109                .into_iter()
110                .map(|inp_outs| inp_outs.into_iter().map(|out| out.key).collect::<Vec<_>>())
111                .collect::<Vec<_>>(),
112        ),
113        TxVersion::RingCT => Rings::RingCT(
114            outputs
115                .into_iter()
116                .map(|inp_outs| {
117                    inp_outs
118                        .into_iter()
119                        .map(|out| [out.key, out.commitment])
120                        .collect::<_>()
121                })
122                .collect::<_>(),
123        ),
124    }
125}
126
127/// Retrieves an [`OutputCache`] for the list of transactions.
128///
129/// The [`OutputCache`] will only contain the outputs currently in the blockchain.
130pub async fn get_output_cache<D: Database>(
131    txs_verification_data: impl Iterator<Item = &TransactionVerificationData>,
132    mut database: D,
133) -> Result<OutputCache, ExtendedConsensusError> {
134    let mut output_ids = IndexMap::new();
135
136    for tx_v_data in txs_verification_data {
137        insert_ring_member_ids(&tx_v_data.tx.prefix().inputs, &mut output_ids)
138            .map_err(ConsensusError::Transaction)?;
139    }
140
141    let BlockchainResponse::Outputs(outputs) = database
142        .ready()
143        .await?
144        .call(BlockchainReadRequest::Outputs(output_ids))
145        .await?
146    else {
147        unreachable!();
148    };
149
150    Ok(outputs)
151}
152
153/// Retrieves the [`TxRingMembersInfo`] for the inputted [`TransactionVerificationData`].
154///
155/// This function batch gets all the ring members for the inputted transactions and fills in data about
156/// them.
157pub async fn batch_get_ring_member_info<D: Database>(
158    txs_verification_data: impl Iterator<Item = &TransactionVerificationData> + Clone,
159    hf: HardFork,
160    mut database: D,
161    cache: Option<&OutputCache>,
162) -> Result<Vec<TxRingMembersInfo>, ExtendedConsensusError> {
163    let mut output_ids = IndexMap::new();
164
165    for tx_v_data in txs_verification_data.clone() {
166        insert_ring_member_ids(&tx_v_data.tx.prefix().inputs, &mut output_ids)
167            .map_err(ConsensusError::Transaction)?;
168    }
169
170    let outputs = if let Some(cache) = cache {
171        Cow::Borrowed(cache)
172    } else {
173        let BlockchainResponse::Outputs(outputs) = database
174            .ready()
175            .await?
176            .call(BlockchainReadRequest::Outputs(output_ids))
177            .await?
178        else {
179            unreachable!();
180        };
181
182        Cow::Owned(outputs)
183    };
184
185    Ok(txs_verification_data
186        .map(move |tx_v_data| {
187            let numb_outputs = |amt| outputs.number_outs_with_amount(amt);
188
189            let ring_members_for_tx = get_ring_members_for_inputs(
190                |amt, idx| outputs.get_output(amt, idx).copied(),
191                &tx_v_data.tx.prefix().inputs,
192            )
193            .map_err(ConsensusError::Transaction)?;
194
195            let decoy_info = if hf == HardFork::V1 {
196                None
197            } else {
198                // this data is only needed after hard-fork 1.
199                Some(
200                    DecoyInfo::new(&tx_v_data.tx.prefix().inputs, numb_outputs, hf)
201                        .map_err(ConsensusError::Transaction)?,
202                )
203            };
204
205            new_ring_member_info(ring_members_for_tx, decoy_info, tx_v_data.version)
206                .map_err(ConsensusError::Transaction)
207        })
208        .collect::<Result<_, _>>()?)
209}
210
211/// Refreshes the transactions [`TxRingMembersInfo`], if needed.
212///
213/// # Panics
214/// This functions panics if `hf == HardFork::V1` as decoy info
215/// should not be needed for V1.
216#[instrument(level = "debug", skip_all)]
217pub async fn batch_get_decoy_info<'a, 'b, D: Database>(
218    txs_verification_data: impl Iterator<Item = &'a TransactionVerificationData> + Clone,
219    hf: HardFork,
220    mut database: D,
221    cache: Option<&'b OutputCache>,
222) -> Result<
223    impl Iterator<Item = Result<DecoyInfo, ConsensusError>> + sealed::Captures<(&'a (), &'b ())>,
224    ExtendedConsensusError,
225> {
226    // decoy info is not needed for V1.
227    assert_ne!(hf, HardFork::V1);
228
229    // Get all the different input amounts.
230    let unique_input_amounts = txs_verification_data
231        .clone()
232        .flat_map(|tx_info| {
233            tx_info.tx.prefix().inputs.iter().map(|input| match input {
234                Input::ToKey { amount, .. } => amount.unwrap_or(0),
235                Input::Gen(_) => 0,
236            })
237        })
238        .collect::<HashSet<_>>();
239
240    tracing::debug!(
241        "Getting the amount of outputs with certain amounts for {} amounts",
242        unique_input_amounts.len()
243    );
244
245    let outputs_with_amount = if let Some(cache) = cache {
246        unique_input_amounts
247            .into_iter()
248            .map(|amount| (amount, cache.number_outs_with_amount(amount)))
249            .collect()
250    } else {
251        let BlockchainResponse::NumberOutputsWithAmount(outputs_with_amount) = database
252            .ready()
253            .await?
254            .call(BlockchainReadRequest::NumberOutputsWithAmount(
255                unique_input_amounts.into_iter().collect(),
256            ))
257            .await?
258        else {
259            unreachable!();
260        };
261
262        outputs_with_amount
263    };
264
265    Ok(txs_verification_data.map(move |tx_v_data| {
266        DecoyInfo::new(
267            &tx_v_data.tx.prefix().inputs,
268            |amt| outputs_with_amount.get(&amt).copied().unwrap_or(0),
269            hf,
270        )
271        .map_err(ConsensusError::Transaction)
272    }))
273}
274
275mod sealed {
276    /// TODO: Remove me when 2024 Rust
277    ///
278    /// <https://rust-lang.github.io/rfcs/3498-lifetime-capture-rules-2024.html#the-captures-trick>
279    pub trait Captures<U> {}
280    impl<T: ?Sized, U> Captures<U> for T {}
281}